mirror of
https://github.com/molstar/molstar.git
synced 2026-06-05 22:31:26 +08:00
Compare commits
601 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b54ff88c | ||
|
|
3e7614d75c | ||
|
|
a01e8f26bd | ||
|
|
351faf3c45 | ||
|
|
8f20571a17 | ||
|
|
c25a4247e6 | ||
|
|
1071d3d8ba | ||
|
|
e8dc046570 | ||
|
|
27f9c2aa67 | ||
|
|
a4962231c8 | ||
|
|
8833f29ce5 | ||
|
|
40b6038380 | ||
|
|
59e16e0187 | ||
|
|
57a790544c | ||
|
|
df0669598c | ||
|
|
9efb5cd126 | ||
|
|
08a56ad6ab | ||
|
|
f500372c16 | ||
|
|
d7ad5a6e9f | ||
|
|
2714d32e15 | ||
|
|
86a74d1cc2 | ||
|
|
3f0f24cb99 | ||
|
|
b8ddc142ea | ||
|
|
cccaa48589 | ||
|
|
3ad355ad40 | ||
|
|
918186eb24 | ||
|
|
db4742cebf | ||
|
|
7d6c77b3bd | ||
|
|
dfcc4e400d | ||
|
|
2d400d9166 | ||
|
|
ebceecb3e6 | ||
|
|
a87f92bf7d | ||
|
|
4033bc93c2 | ||
|
|
6c4ba7af61 | ||
|
|
bc9584e49b | ||
|
|
c9734d83a2 | ||
|
|
93943cc27b | ||
|
|
25836b2de0 | ||
|
|
c6874c922d | ||
|
|
550d898c4f | ||
|
|
5e16c340dc | ||
|
|
bcb18a8faf | ||
|
|
cd7d8f704e | ||
|
|
0d197b2dc5 | ||
|
|
b3ce268f0e | ||
|
|
2da02daadc | ||
|
|
999e5a47af | ||
|
|
ff3fad0789 | ||
|
|
5af6265c07 | ||
|
|
4ad7a96191 | ||
|
|
71215d183d | ||
|
|
e864f13a66 | ||
|
|
59298be573 | ||
|
|
2a57867167 | ||
|
|
a817a70b46 | ||
|
|
5afb7981ae | ||
|
|
2308dfd22e | ||
|
|
f0de2cea2c | ||
|
|
8f9c687935 | ||
|
|
1f56405d79 | ||
|
|
37af8c66a1 | ||
|
|
a1fefa2efa | ||
|
|
958f3011e4 | ||
|
|
8d48fa67ae | ||
|
|
87158db7c0 | ||
|
|
bac65cc71e | ||
|
|
3ece0c74c6 | ||
|
|
89fbb690fe | ||
|
|
918d02fec4 | ||
|
|
8af3a240b5 | ||
|
|
a80284ac02 | ||
|
|
1b48f4c32a | ||
|
|
1055eab4c5 | ||
|
|
d4445cef5c | ||
|
|
063d327a5f | ||
|
|
34f409e683 | ||
|
|
8415ed1b92 | ||
|
|
c92147289e | ||
|
|
5b16213cd2 | ||
|
|
bbd36e1838 | ||
|
|
1a0b30d6eb | ||
|
|
e309e8917a | ||
|
|
14135b8386 | ||
|
|
3230a6a7dc | ||
|
|
34ebc5ab7a | ||
|
|
d8b62c5cbb | ||
|
|
358ef44780 | ||
|
|
fb2f79a395 | ||
|
|
e3a95e0a08 | ||
|
|
a1b09ccc1c | ||
|
|
26b31b3fcc | ||
|
|
3027418d31 | ||
|
|
5a69fb691d | ||
|
|
cced98c93f | ||
|
|
6c299161fe | ||
|
|
10575ac361 | ||
|
|
d715330d8e | ||
|
|
fdba049982 | ||
|
|
270d7386b2 | ||
|
|
a28c2f0995 | ||
|
|
196e17ff0d | ||
|
|
c7efac0a78 | ||
|
|
75eb04070c | ||
|
|
d6c9ae1fbe | ||
|
|
24a6403025 | ||
|
|
842e5d890e | ||
|
|
b34d1cca00 | ||
|
|
af4dc090c4 | ||
|
|
1fa090d162 | ||
|
|
4d5b749e3e | ||
|
|
6a736eb89f | ||
|
|
cfead0481f | ||
|
|
fbbd7e623e | ||
|
|
f16707b849 | ||
|
|
76b0b23c07 | ||
|
|
d4a2bd7cba | ||
|
|
c572feb1d2 | ||
|
|
121f8eab3e | ||
|
|
8c49b82c3d | ||
|
|
ac7faf8524 | ||
|
|
5d6d91a331 | ||
|
|
d476db556d | ||
|
|
354d092834 | ||
|
|
5cde26a8e2 | ||
|
|
b905178395 | ||
|
|
543d014d0d | ||
|
|
a17afa59b3 | ||
|
|
bd6be354d5 | ||
|
|
2058d605c7 | ||
|
|
ba60188758 | ||
|
|
52f2ddf715 | ||
|
|
aa20fffbfb | ||
|
|
b59d11c91a | ||
|
|
8fdc29d048 | ||
|
|
d52ea41051 | ||
|
|
852be261dd | ||
|
|
95e9a3012d | ||
|
|
dd0a45c154 | ||
|
|
4692d63a2b | ||
|
|
0689ecabb6 | ||
|
|
424f576e99 | ||
|
|
4407994195 | ||
|
|
4c6331e72d | ||
|
|
825514dd10 | ||
|
|
8e2967b993 | ||
|
|
a46ba63e31 | ||
|
|
cf9fe99a81 | ||
|
|
2909a209c3 | ||
|
|
1322882444 | ||
|
|
119c548fa7 | ||
|
|
539442f710 | ||
|
|
8841f04af6 | ||
|
|
8c2d3a577a | ||
|
|
bed4b728d3 | ||
|
|
aa87acc0a7 | ||
|
|
7d1e2b44db | ||
|
|
c1c1badf62 | ||
|
|
0937c84f47 | ||
|
|
6a7f892d60 | ||
|
|
b4cd2d0a11 | ||
|
|
2067f02830 | ||
|
|
6d86ada6b4 | ||
|
|
f656cf09b7 | ||
|
|
a891b4c551 | ||
|
|
ded844c936 | ||
|
|
44b36637fd | ||
|
|
f590bd0f0a | ||
|
|
9474c80673 | ||
|
|
7b48d691c8 | ||
|
|
b03146852f | ||
|
|
e270a83909 | ||
|
|
a40b737c6f | ||
|
|
942533ed2b | ||
|
|
546f3cd3c5 | ||
|
|
21597b1fdd | ||
|
|
31d8568c1a | ||
|
|
3630cd14e8 | ||
|
|
4f083f10e6 | ||
|
|
371ef984c0 | ||
|
|
e2db1257cd | ||
|
|
c812e72a1a | ||
|
|
ef9b89820d | ||
|
|
9345f3584a | ||
|
|
4d058aa1a8 | ||
|
|
e7da6092aa | ||
|
|
94f6b864b0 | ||
|
|
6e90447511 | ||
|
|
b91030c4bd | ||
|
|
5fd453c77a | ||
|
|
7f2b10674e | ||
|
|
238e5e0b88 | ||
|
|
1f26b5c339 | ||
|
|
eac478e7cb | ||
|
|
d0c59fdc92 | ||
|
|
7e61bcad32 | ||
|
|
7e98870dce | ||
|
|
405dc0d90a | ||
|
|
7a362c816e | ||
|
|
7e1396b74c | ||
|
|
68ad1ec065 | ||
|
|
430f8da44e | ||
|
|
68866cd2de | ||
|
|
05888bec50 | ||
|
|
65e1cb4a5d | ||
|
|
50f571b0d3 | ||
|
|
d86b31edf8 | ||
|
|
ec107352b4 | ||
|
|
1d42d5a2d6 | ||
|
|
02d1dcb9d9 | ||
|
|
d86c3621b7 | ||
|
|
f2724491c2 | ||
|
|
f4c84a6930 | ||
|
|
0eb9b286b4 | ||
|
|
da006391da | ||
|
|
130e33f8c3 | ||
|
|
109e528d1c | ||
|
|
5df69cd84a | ||
|
|
973afa2237 | ||
|
|
0088d3e1bf | ||
|
|
26e6a11fa8 | ||
|
|
056e2c5182 | ||
|
|
0e7cde24bc | ||
|
|
36ce262970 | ||
|
|
289c8181c8 | ||
|
|
eb0fd490d4 | ||
|
|
70c073c43c | ||
|
|
0565df4df9 | ||
|
|
6dd425cb55 | ||
|
|
c0f994a506 | ||
|
|
63705ed158 | ||
|
|
fda9069f17 | ||
|
|
66bffd8403 | ||
|
|
4a7d83c85b | ||
|
|
fef649ce09 | ||
|
|
794f81bb8e | ||
|
|
bc5648620d | ||
|
|
07897f57f3 | ||
|
|
77f756dfe0 | ||
|
|
15eef7b688 | ||
|
|
5d0ba7504b | ||
|
|
8d59b5b814 | ||
|
|
86871124d5 | ||
|
|
1a328d98b6 | ||
|
|
fcbc3ab3d0 | ||
|
|
f36093dad9 | ||
|
|
d4c2bb85cb | ||
|
|
d53c1e8e65 | ||
|
|
f189d0bdab | ||
|
|
395eddd927 | ||
|
|
eb1d48a73c | ||
|
|
38b0bb8d7d | ||
|
|
0daffa6b57 | ||
|
|
e1d5d369f1 | ||
|
|
6b88acd2bc | ||
|
|
6384eac7e7 | ||
|
|
fd409ce27f | ||
|
|
c21c9f5160 | ||
|
|
71d00a22dd | ||
|
|
5554697028 | ||
|
|
709ac8430a | ||
|
|
2ee08f6161 | ||
|
|
3608578528 | ||
|
|
5114a211fd | ||
|
|
8cc947c998 | ||
|
|
9105737834 | ||
|
|
84bcbd1ca6 | ||
|
|
144967dbd3 | ||
|
|
bdb33b9398 | ||
|
|
ade6ef5631 | ||
|
|
3bc60d1d59 | ||
|
|
4068c45eb4 | ||
|
|
52d0ff4a67 | ||
|
|
f346d15bef | ||
|
|
b2ce1fc6fa | ||
|
|
4e331001ef | ||
|
|
7d67304e4c | ||
|
|
babda601cb | ||
|
|
8bdfff5e94 | ||
|
|
be4b408ddc | ||
|
|
230697fbb4 | ||
|
|
78ab6b0c95 | ||
|
|
31819dbf16 | ||
|
|
1f0c24b58e | ||
|
|
f5c587bfe5 | ||
|
|
52b1c7e4d9 | ||
|
|
e76d02bc8c | ||
|
|
481b763049 | ||
|
|
7bfef2ae40 | ||
|
|
01fe10ebdc | ||
|
|
1665dd7d00 | ||
|
|
4e4b80a7b2 | ||
|
|
9716fecdb9 | ||
|
|
684fd2d237 | ||
|
|
9432b9a7a7 | ||
|
|
3a37c95c17 | ||
|
|
6040b99c19 | ||
|
|
83bef0f0e7 | ||
|
|
95bb3a1f81 | ||
|
|
be677f47cb | ||
|
|
43bf69d09c | ||
|
|
b6cc626431 | ||
|
|
931fdfca9b | ||
|
|
1c10db5656 | ||
|
|
c4ccd8758f | ||
|
|
6c99c575bc | ||
|
|
ae2493b6e3 | ||
|
|
bcd50c294f | ||
|
|
9c0024dbab | ||
|
|
c15b3603c0 | ||
|
|
70647ba972 | ||
|
|
8d19357845 | ||
|
|
8e9817c4d1 | ||
|
|
b16147b88c | ||
|
|
9840d8f816 | ||
|
|
d892ccab4c | ||
|
|
65f88b3293 | ||
|
|
9e6e5eb795 | ||
|
|
2f755efeec | ||
|
|
012e616ec4 | ||
|
|
007d0e7608 | ||
|
|
bf313073b9 | ||
|
|
293928f3de | ||
|
|
2404f398b6 | ||
|
|
43ff6e24c8 | ||
|
|
9e62112366 | ||
|
|
026d6fc618 | ||
|
|
95fcd942dc | ||
|
|
805481db14 | ||
|
|
39175df025 | ||
|
|
cd0f451f6b | ||
|
|
fe1aa1a9bf | ||
|
|
fcfb6e6d5a | ||
|
|
c548c94575 | ||
|
|
2d45f4a77c | ||
|
|
a5ae887842 | ||
|
|
e4b53cdc6a | ||
|
|
c53940e67e | ||
|
|
6d61745f0f | ||
|
|
46d86d93b0 | ||
|
|
11772b64fb | ||
|
|
dbc8ab00c6 | ||
|
|
015fad4371 | ||
|
|
71a484586f | ||
|
|
f0b06ee746 | ||
|
|
b0694b886b | ||
|
|
eaf47b3169 | ||
|
|
ad9046fcf2 | ||
|
|
eabe4d46bc | ||
|
|
003c5f8fb7 | ||
|
|
68748a4a94 | ||
|
|
9bd6b8195d | ||
|
|
05848b651c | ||
|
|
0a8f87dd9f | ||
|
|
925aaa701d | ||
|
|
5be599bad4 | ||
|
|
e22ce53e65 | ||
|
|
4c49431027 | ||
|
|
4192d82ef3 | ||
|
|
ce220737f2 | ||
|
|
eeb7cd2c52 | ||
|
|
748111beb2 | ||
|
|
1f7d41c653 | ||
|
|
b9430ff387 | ||
|
|
6591bab035 | ||
|
|
4da446aec2 | ||
|
|
25c170e36d | ||
|
|
eba18d1dce | ||
|
|
2c87d01a5e | ||
|
|
e41a2baa32 | ||
|
|
c297017749 | ||
|
|
9a0fc1faa6 | ||
|
|
424513f23c | ||
|
|
895d672589 | ||
|
|
0c6253ed16 | ||
|
|
da97cd20aa | ||
|
|
ca6d73e048 | ||
|
|
88b79deefa | ||
|
|
d756e2e195 | ||
|
|
2ce126a8f5 | ||
|
|
01e95dada0 | ||
|
|
1c024f0943 | ||
|
|
5901e3d6a1 | ||
|
|
0cfe1cec66 | ||
|
|
c1930e4142 | ||
|
|
71375d908f | ||
|
|
728b87d4e4 | ||
|
|
9c17698a8a | ||
|
|
625381c446 | ||
|
|
da949a245e | ||
|
|
7000bdd15d | ||
|
|
adcf6a6fa8 | ||
|
|
b70af9f178 | ||
|
|
e5bdcfd781 | ||
|
|
6049705224 | ||
|
|
273d50d403 | ||
|
|
333ea724d6 | ||
|
|
e96dca91ef | ||
|
|
41a0048f64 | ||
|
|
5e97b05bd2 | ||
|
|
ebc6b2acce | ||
|
|
8372408d9c | ||
|
|
2c6822f5ab | ||
|
|
7efbf46e7a | ||
|
|
b6d6a518d3 | ||
|
|
2d690268f9 | ||
|
|
e0c794b557 | ||
|
|
f91f445631 | ||
|
|
1cc367c8d8 | ||
|
|
8c6969206d | ||
|
|
c0479e3d46 | ||
|
|
22e92b38c6 | ||
|
|
5741709023 | ||
|
|
2265fc02cc | ||
|
|
64180bef36 | ||
|
|
be3caef6e9 | ||
|
|
71a2f71866 | ||
|
|
3c6152054e | ||
|
|
080d649bf9 | ||
|
|
2852b09c77 | ||
|
|
5e53467541 | ||
|
|
42dc579ddb | ||
|
|
890c758585 | ||
|
|
e6c77069df | ||
|
|
e7ecf98f13 | ||
|
|
70ad32f62d | ||
|
|
69fe452055 | ||
|
|
9edeb84f4e | ||
|
|
e1db3114c8 | ||
|
|
8724badcb6 | ||
|
|
d413f74526 | ||
|
|
6752108c5f | ||
|
|
9302fdadb9 | ||
|
|
f7048c7535 | ||
|
|
3252a3f0f3 | ||
|
|
6805194d48 | ||
|
|
acf0dceb47 | ||
|
|
c53f500da6 | ||
|
|
defc04278e | ||
|
|
aa4d5e78a7 | ||
|
|
df3a432afd | ||
|
|
1b339d18cc | ||
|
|
c4650c91a8 | ||
|
|
e3c4f19563 | ||
|
|
85780a5d6a | ||
|
|
aab70e2ff0 | ||
|
|
e859f497f1 | ||
|
|
6a9fed56f3 | ||
|
|
d7c2505852 | ||
|
|
754dfeab91 | ||
|
|
d3b02df5b9 | ||
|
|
3d95ed729c | ||
|
|
9cbb4414e0 | ||
|
|
79fcfe50bc | ||
|
|
216d16456b | ||
|
|
822aaa99b0 | ||
|
|
2c683ab77d | ||
|
|
2ef5af6881 | ||
|
|
36f18be042 | ||
|
|
f093a3ab37 | ||
|
|
74cd42117b | ||
|
|
bb4a4e6102 | ||
|
|
24a3167f9b | ||
|
|
214e1c20ca | ||
|
|
33cab6ddad | ||
|
|
f4b2826bc7 | ||
|
|
ebaa9f2e56 | ||
|
|
812b75a034 | ||
|
|
3b02a5f5ec | ||
|
|
657d2eb1c5 | ||
|
|
25d87dd14d | ||
|
|
d2605e6e3d | ||
|
|
b21ebe0f55 | ||
|
|
2693fe8b7e | ||
|
|
45279a6520 | ||
|
|
22f9b1a7a1 | ||
|
|
8325a58e25 | ||
|
|
0acc508a8f | ||
|
|
2af0cd9d6f | ||
|
|
304858fcba | ||
|
|
ade027911c | ||
|
|
a97e647f7a | ||
|
|
008bed0233 | ||
|
|
bb4c04f3b9 | ||
|
|
62997e5972 | ||
|
|
a20e7bb40d | ||
|
|
2acfac4c85 | ||
|
|
a1a9d87a54 | ||
|
|
1ab71cc487 | ||
|
|
a8b19f5f3c | ||
|
|
4661a4a5f0 | ||
|
|
2c40abc808 | ||
|
|
10d7bcf4c0 | ||
|
|
5f8e4e6913 | ||
|
|
94fa9f124a | ||
|
|
3e70251f38 | ||
|
|
66ed6cfa94 | ||
|
|
d82b6e8d0d | ||
|
|
5a5f6867b9 | ||
|
|
5cd5fc09f5 | ||
|
|
17528d5ca2 | ||
|
|
e658a11947 | ||
|
|
4ac6f5c202 | ||
|
|
5726515707 | ||
|
|
f2ee7d1470 | ||
|
|
4140412e06 | ||
|
|
44ed142521 | ||
|
|
1ae0bbc150 | ||
|
|
8213611293 | ||
|
|
2697634a9f | ||
|
|
d7ba9e0c61 | ||
|
|
c99c4342b7 | ||
|
|
f410e27d1a | ||
|
|
e6d54412cf | ||
|
|
6238684819 | ||
|
|
ea07cd89de | ||
|
|
a7330f40d7 | ||
|
|
92c55ffe35 | ||
|
|
c21ba08fc7 | ||
|
|
ba3a716900 | ||
|
|
3133dc1543 | ||
|
|
fe2541f9e8 | ||
|
|
27af73f97f | ||
|
|
e9a442ca6e | ||
|
|
e86e282bb4 | ||
|
|
213506dff0 | ||
|
|
bc7aa7c9aa | ||
|
|
b234bf8890 | ||
|
|
36b4dcf7a8 | ||
|
|
0e843c20cc | ||
|
|
ecaf19c5fb | ||
|
|
f024aeef2c | ||
|
|
9d9985f117 | ||
|
|
a0f7349ef6 | ||
|
|
01407427d2 | ||
|
|
3dee03d9b6 | ||
|
|
737f6593be | ||
|
|
068e10dd40 | ||
|
|
c1ba5248b0 | ||
|
|
4af0f22ac0 | ||
|
|
25a67e1176 | ||
|
|
a8fcd501d6 | ||
|
|
573ee92889 | ||
|
|
2558d6fada | ||
|
|
2cf3f8d62b | ||
|
|
589d89b0d5 | ||
|
|
7cc7b77460 | ||
|
|
e8a9995bef | ||
|
|
74ff283e00 | ||
|
|
1ecb960b82 | ||
|
|
387d59f97b | ||
|
|
d993082f24 | ||
|
|
5eaa73d56d | ||
|
|
b9428fd3cd | ||
|
|
97d180b79d | ||
|
|
25bd915ea5 | ||
|
|
f8fdffdc44 | ||
|
|
d11aa6ea77 | ||
|
|
fc3c7997ea | ||
|
|
b3aecf8de4 | ||
|
|
f3581e62ef | ||
|
|
88e7fe508f | ||
|
|
98049ed02d | ||
|
|
194092ed67 | ||
|
|
e96157c890 | ||
|
|
a028c1ef42 | ||
|
|
ad2b5e687d | ||
|
|
8ba19f0be4 | ||
|
|
bccc68f6df | ||
|
|
026a05d03d | ||
|
|
2b4741c8ee | ||
|
|
7960ee06d4 | ||
|
|
f73f5af131 | ||
|
|
3123110aa4 | ||
|
|
154063638d | ||
|
|
a720b98365 | ||
|
|
d4a2937e0b | ||
|
|
b0ca7ffbb7 | ||
|
|
c42b738abe | ||
|
|
ab0d0fec53 | ||
|
|
8d96131962 | ||
|
|
95bbcd8b24 | ||
|
|
a21f5c2c23 | ||
|
|
94b7b1281c | ||
|
|
16dba586df | ||
|
|
72b761f959 | ||
|
|
943d81cbf9 | ||
|
|
2ecdc0eafa | ||
|
|
dccfd35c7a | ||
|
|
9e81a4f7a6 | ||
|
|
6f6cc73ce9 | ||
|
|
c248ae11bf | ||
|
|
742be03901 | ||
|
|
00009ef198 | ||
|
|
1cb617524d | ||
|
|
e2e348240b | ||
|
|
b54908492c | ||
|
|
33172862bd | ||
|
|
ea612c3acb | ||
|
|
a1308645e5 | ||
|
|
794b705184 | ||
|
|
66264abe50 |
14
.git-blame-ignore-revs
Normal file
14
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,14 @@
|
||||
# added semicolons to linting rules
|
||||
fb0634a0f4aab3764b7e6368e38d8dea7615e591
|
||||
|
||||
# new linting rules (no default exports, no named tuples)
|
||||
6c5224f33e9de20fe9967a82536c269bacf29738
|
||||
|
||||
# lint: add space-in-parens rule
|
||||
1d21787e7ea1971817813c008351541e4640c261
|
||||
|
||||
# lint: add object-curly-spacing rule
|
||||
b31302ba3ad4ab7f98aedd500b762be642374ff0
|
||||
|
||||
# fix eslint warnings
|
||||
3b1513adc0048dc4879f1d70874b3e56aaffd10e
|
||||
204
CHANGELOG.md
204
CHANGELOG.md
@@ -4,6 +4,208 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
- Fix empty transforms default in `ShapeFromPly`
|
||||
- Add `Camera.changed` event and rotation/translation setter/getter
|
||||
- Add `instanceGranularity: 'auto'` as a memory guard
|
||||
- Honor `instanceGranularity` in `Visual.getLoci`
|
||||
- Add mesoscale representation preset
|
||||
- Add presets option to `ObjectList` param definition
|
||||
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
|
||||
- Adds File/Open and drag-and-drop support for Kinemage files in the viewer app
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
- Add 8K UHD option to `ViewportScreenshotHelper`
|
||||
- Handle MRC files with empty length header fields
|
||||
- Handle CCD bonds with Deuterium atoms
|
||||
- [Breaking] ComponentBond.Entry.map now returns ComponentBond.Pairs
|
||||
- Fix volume slice marking performance regression
|
||||
- Add GPU procedural animation (wiggle & tumble)
|
||||
- Per-vertex wiggle via fbm noise (position & group mode)
|
||||
- Per-instance tumble via fbm noise (rotation + translation)
|
||||
- `Wiggle` theme layer for data-driven per-group wiggle
|
||||
- `enableAnimation` Canvas3D param for global toggle
|
||||
- Add `AnimateTime` built-in for, e.g., exporting procedural animation
|
||||
- Add Procedural Animation panels
|
||||
- Viewer: structure dynamics & uncertainty
|
||||
- Mesoscale Explorer: entity dynamics
|
||||
- Fix `GraphQLClient` missing required headers
|
||||
- [Breaking] Use Record instead of Array for headers (assets & data-source utils)
|
||||
|
||||
## [v5.8.0] - 2026-04-03
|
||||
- Dependencies: remove `utils.promisify`, `node-fetch` (#1797)
|
||||
- Fix circular dependency which causes crash in bundlers (#1791)
|
||||
- Add `putty` as a mol-view-spec representation.
|
||||
- Fix detecting sidechain-only structures as coarse-grained (#1420)
|
||||
- Fix clip-object transform due to missing axis normalization
|
||||
- Sequence alignment: Fix return type & improve scoring for unknown residues
|
||||
- Use PDB SEQRES block to show unresolved residues in Sequence toolbar
|
||||
- Canvas3D debug-helpers
|
||||
- [Breaking] Move helpers to an extension as a PluginBehavior (params are no longer part of Canvas3D)
|
||||
- Add helpers for clip-object, direct-volume, image, mesh
|
||||
- Fix StructureComponent node update throwing error when substructure empty
|
||||
- CSS: Avoid tooltip box flickering when hovering something under it
|
||||
- Volume slice visual
|
||||
- Fix support for volume instances
|
||||
- Fix plane mode: ensure normalized & correctly oriented
|
||||
- MolViewSpec
|
||||
- Add `VolumeStreamingExtension` (`molstar_volume_streaming` custom property)
|
||||
- Fix focusing empty selections
|
||||
- Avoid re-calculating static model properties for trajectories
|
||||
|
||||
## [v5.7.0] - 2026-02-28
|
||||
- Text label improvements
|
||||
- Improve label background vertical centering
|
||||
- Handle label depth variant for correct transparent background
|
||||
- Draw border under text using fragment depth to prevent overlap on adjacent characters
|
||||
- Clamp border width to avoid exceeding SDF range
|
||||
- Increase font atlas quality (2x font size multiplier)
|
||||
- TM-align performance improvements (#1745)
|
||||
- Disable transparent outline close to opaque elements
|
||||
- Add axis param to trackball spin & rock animation
|
||||
- Color smoothing fixes (#1747)
|
||||
- Use correct instance for non instance-type
|
||||
- Never transform for non instance-type
|
||||
- Add extra radius to gaussian surface boundingsphere
|
||||
- MolViewSpec
|
||||
- Add `MVSData.toMVSX` function and `mvs-mvsj-to-mvsx.js` CLI utility
|
||||
- [Breaking] Add PQR file format support (#157)
|
||||
- Replace `isPdbqt` with `variant` param in `TrajectoryFromPDB`
|
||||
- Add `CustomVolumeProperty` (like for models and structures)
|
||||
- Geometry export
|
||||
- Fix missing `usePalette` support
|
||||
- Fix vertex-based coloring for non-mesh geometries
|
||||
- Support line-strips
|
||||
- Support vertex-based sizing
|
||||
- Support memory efficient line-strips in Lines geometry,
|
||||
- Add `StripLinesBuilder`
|
||||
- Add `computeFrenetFrames` helper
|
||||
- Streamlines support
|
||||
- Add basic calculation method
|
||||
- Add custom-volume-property
|
||||
- Add representation with lines and tube-mesh visuals
|
||||
- Fix `TextCtrl` always moving cursor to end position
|
||||
- Add `vertex` and `vertexInstance` granularity support for size themes
|
||||
- Add `transform` and `domain` parameters to volume-value size theme
|
||||
- Fix parsing of single charge type_symbols (e.g., N+) in cif-core
|
||||
- Detect metal-coordination when parsing pdb
|
||||
- Handle additional elements in `guessElementSymbol*` (As, Li, Ga)
|
||||
- Add more element-pair thresholds for bonding (Ag-S, CoSb, Ga-F)
|
||||
- Add `metalCoordination` style param (dashed, solid) for bonds
|
||||
- Fix `unitSymmetryGroups` for representations with `includeParent` enabled
|
||||
- Add `convexHull` helper
|
||||
- Add `Structure.coordination` sites
|
||||
- Add `Polyhedron` representation showing coordination sites
|
||||
- Guard against `xr-spatial-tracking` blocked in `Permissions-Policy`
|
||||
|
||||
## [v5.6.1] - 2026-01-23
|
||||
- Disable occlusion culling in `ImagePass` (#1758)
|
||||
- MolViewSpec
|
||||
- Fix `MVSAnnotationStructureComponent` not updating properly when parent structure changes
|
||||
|
||||
## [v5.6.0] - 2026-01-18
|
||||
- Handle Hex codes that are submitted with alpha channels by ignoring the alpha channel (#1746)
|
||||
- Only show "already registered transformer" warnings in non-production builds
|
||||
- Fix `label_seq_id` assignment in PDB parser to use 1-based linear indexing (#1730) if:
|
||||
- when insertion codes are present
|
||||
- `SEQRES` records are present
|
||||
- Viewer app
|
||||
- Add `action: 'focus'` support to `Viewer.structureInteractivity`
|
||||
- Add `viewportFocusBehavior: 'secondary-zoom'`
|
||||
- MolViewSpec
|
||||
- Validation treats `undefined` same as missing value
|
||||
- Increase default size of `carbohydrate` representation
|
||||
- `color_from_uri` and `color_from_source` take `selector` parameter
|
||||
- Add `keepCameraOrientation` option for loading functions
|
||||
- `label_from_*` and `tooltip_from_*` take `text_format` parameter
|
||||
- `label_from_*` take `group_by_fields` parameter
|
||||
- Tweak Gaussian Density smoothness default range (less artefacts)
|
||||
- Support `includeParent` for Gaussian Surface (disables GPU support)
|
||||
- Support floodfill before surface extraction (`off`, `interior`, `exterior`)
|
||||
- For Isosurface, Molecular Surface, Gaussian Surface
|
||||
- Fix `to_mmCIF` writing duplicate categories under certain conditions (#1738)
|
||||
- Add stable random number generator (PCG)
|
||||
- ME grayscale colors; dot offset; SSAO hemisphere vectors
|
||||
- Use blue noise for SSAO hemisphere vectors
|
||||
- Fix SSAO darkening when sampling background/offscreen pixels
|
||||
- Adding structure wireframe visuals on molecular and gaussian surfaces
|
||||
- Fix caching of `__srcIndexArray__`
|
||||
- Prevent self-occlusion on quaternary amine
|
||||
- Fix outline postprocessing artifacts (black bands) on membrane layers at grazing view angles in Illustrative mode (#1749)
|
||||
- Remove fence from `Canvas3D.render` to not interfer with `requestAnimationFrame`
|
||||
- Fix boundingSphere reuse in structure visuals (was triggering extra calculation)
|
||||
- Use PDB seqres record to deduce entity information
|
||||
- Add lipid components names used in amber ff
|
||||
|
||||
## [v5.5.0] - 2025-12-22
|
||||
- Viewer app
|
||||
- Move viewer extensions, options, and presets to a separate file
|
||||
- Add `molstar.lib` export providing access to a wide range of functionality previously not available from the compiled bundle
|
||||
- Add `Viewer.subscribe` method that keeps track of subscribed plugin events and disposes them together with the parent viewer
|
||||
- Add `Viewer.structureInteractivity` that makes it easy to highlight/select elements on the loaded structure
|
||||
- Add `viewportBackgroundColor` and `viewportFocusBehavior` options
|
||||
- Add `mvs.html` example to showcase the new functionality combined with MolViewSpec
|
||||
- Add dark and blue color theme support (import `theme/dark.css` or `theme/blue.css` instead of the default `molstar.css`)
|
||||
- MolViewSpec extension
|
||||
- Add `tryGetPrimitivesFromLoci` that makes it easier to access primitive element data from hover/click interactions
|
||||
- Add `getCurrentMVSSnapshot` to obtain source data for the currently displayed snapshot
|
||||
- Add TM-align structure-based protein alignment algorithm
|
||||
- New `TMAlign` namespace in `mol-math/linear-algebra/3d/tm-align.ts`
|
||||
- New `tmAlign` function in `mol-model/structure/structure/util/tm-align.ts`
|
||||
- Returns TM-score, RMSD, alignment mapping, and transformation matrix
|
||||
- Molecular Surface
|
||||
- Fix "auto" quality params not hidden
|
||||
- Fix calculation when probe diameter is smaller then resolution
|
||||
- Fix webgl1 shader syntax
|
||||
- Fix program not compiled for sync picking
|
||||
- Fix missing `gl.flush` for async picking (needed for Safari)
|
||||
- Add Residue Charge color scheme (#1722)
|
||||
- Add dropdown indicator for mapped parameter definitions and adjust "more options" icon
|
||||
- Fix `flipSided` for meshes
|
||||
- [Breaking] Interior coloring
|
||||
- Remove global `interiorDarkening`, `interiorColorFlag`, `interiorColor`
|
||||
- Add per-geometry `interiorColor`, `interiorSubstance`
|
||||
- Add `label/auth_comp_id` to `StructureProperties.residue`
|
||||
- Previously, this has been only been present on `.atom` (since residue name can alter on per-atom basis), but this has been a bit confusing for the general use-case
|
||||
- Move canvas "checkered background" logic to `canvas3d.ts` and only apply it when `transparentBackground` is on
|
||||
- This prevents ugly flickering during plugin initialization
|
||||
- Fix unit hash collision issues (#1721)
|
||||
|
||||
## [v5.4.2] - 2025-12-07
|
||||
- Fix postprocessing issues with SSAO and outlines for large structures (#1387)
|
||||
- Reduce automatic quality on standalone HMD devices
|
||||
|
||||
## [v5.4.1] - 2025-11-16
|
||||
- Fix ugly camera clipping in snapshot transitions
|
||||
- Add viewport button to toggle illumination mode
|
||||
- Fix bounding sphere computation for 3D text
|
||||
- Structure bounding sphere includes atom VDW radii / coarse sphere radii
|
||||
- Relax camera limits to allow focusing any selection with >1 atom
|
||||
- MolViewSpec
|
||||
- Fix `appendSnapshots` when loading MVSX
|
||||
- Fix all-selector color not applying on substructure
|
||||
- Fix primitives in root not being transformed with reference structure
|
||||
- Color themes do not prefer smoothing (improves performance in animations)
|
||||
- Allow canvas background interpolation
|
||||
- Fix `direct-volume` not drawn in illumination mode
|
||||
- Fix default trackball animated spin speed
|
||||
- Use `PluginCommands` to set canvas3d props in camera behavior
|
||||
- Volume improvements
|
||||
- Add `Volume.periodicity`
|
||||
- Wrap isosurfaces for periodic volumes
|
||||
- Fix dimensions for slices
|
||||
- Add support for Input Method Editor (IME) to text params input
|
||||
- Update `guessCifVariant` to detect density files not generated by the VolumeServer
|
||||
|
||||
## [v5.3.0] - 2025-11-05
|
||||
- Update loading message in MVS Stories Viewer
|
||||
- Add `Canvas3D.setAttribs`
|
||||
- Fix `normalizeWheel` "spin" calculation fallback
|
||||
- MolViewSpec
|
||||
- Add support for "topology" formats (TOP, PRMTOP, PSF)
|
||||
- Add support for additional "coordiates" formats (NCTRAJ, DCD, TRR)
|
||||
- Fix coarse structure selection
|
||||
- Fix missing default param values in `primitives_from_uri`
|
||||
|
||||
## [v5.2.0] - 2025-10-31
|
||||
- Handle transparency updates on ImagePass
|
||||
@@ -15,7 +217,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add clipping support for primitives
|
||||
- Support near camera distance
|
||||
|
||||
## [v5.1.0] - 2025-10-25
|
||||
## [v5.1.2] - 2025-10-25
|
||||
- Fix createColorScaleByType when offsets are available
|
||||
- Get bond orders from non-standard CONECT records in PDB files
|
||||
- Remove outdated `gl_FrontFacing` workaround for buggy drivers
|
||||
|
||||
@@ -126,16 +126,16 @@ and navigate to `build/viewer`
|
||||
|
||||
**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
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
|
||||
**Saccharide names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-saccharides.js src/mol-model/structure/model/types/saccharides.ts
|
||||
|
||||
### Other scripts
|
||||
**Create chem comp bond table**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
node --max-old-space-size=8192 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
|
||||
**Test model server**
|
||||
|
||||
|
||||
1
breaking-v6-changes.md
Normal file
1
breaking-v6-changes.md
Normal file
@@ -0,0 +1 @@
|
||||
- Remove `checkeredCanvasBackground` from `PluginContext` and `PluginContainer`
|
||||
@@ -14,6 +14,7 @@ chemical.melting_point
|
||||
|
||||
chemical_formula.moiety
|
||||
chemical_formula.sum
|
||||
chemical_formula.iupac
|
||||
chemical_formula.weight
|
||||
|
||||
atom_type.symbol
|
||||
@@ -25,6 +26,8 @@ atom_type_scat.source
|
||||
|
||||
space_group.crystal_system
|
||||
space_group.name_h-m_full
|
||||
space_group.name_h-m_alt
|
||||
space_group.name_hall
|
||||
space_group.it_number
|
||||
space_group_symop.operation_xyz
|
||||
|
||||
|
||||
|
@@ -33,11 +33,11 @@ npm run build
|
||||
For a watch task to automatically rebuild the source code on changes, run
|
||||
|
||||
```
|
||||
npm run watch
|
||||
npm run dev
|
||||
```
|
||||
|
||||
or if working just with the Viewer app for better performance
|
||||
|
||||
```
|
||||
npm run watch-viewer
|
||||
npm run dev:viewer
|
||||
```
|
||||
|
||||
@@ -48,4 +48,7 @@
|
||||
* CLR (e.g. 3GKI) - four fused rings
|
||||
* Assembly symmetries
|
||||
* 5M30 (Assembly 1, C3 local and pseudo)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* Deuterium atoms
|
||||
* 3CWH (XUL with D and DOD)
|
||||
* 8TT8 (HOH and other with D)
|
||||
@@ -1,6 +1,6 @@
|
||||
# Building a Custom Library
|
||||
|
||||
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page.
|
||||
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page using the `esbuild` tool.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -15,10 +15,33 @@ There are 4 basic ways of instantiating the Mol* plugin.
|
||||
|
||||
## ``Viewer`` wrapper
|
||||
|
||||
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require much custom behavior and are mostly about just displaying a structure.
|
||||
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods and options.
|
||||
- The most basic usage is to use the ``Viewer`` wrapper. This is best suited for use cases that do not require custom behavior and are mostly about just displaying a structure.
|
||||
- See ``Viewer`` class is defined in [src/apps/viewer/app.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/app.ts) for available methods
|
||||
- See [options.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) for available plugin options
|
||||
- See [embedded.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/embedded.html) and [mvs.html](https://github.com/molstar/molstar/blob/master/src/apps/viewer/mvs.html) for example usage
|
||||
- Importing `molstar.js` will expose `molstar.lib` namespace that allow accessing various functionality without a bundler such as WebPack or esbuild. See the `mvs` example above for basic usage.
|
||||
- Alternative color themes can be used by importing `theme/dark.css` (or `light/blue`) instead of `molstar.css`
|
||||
|
||||
Example usage without using WebPack:
|
||||
### molstar.js and molstar.css sources
|
||||
|
||||
- Download `molstar` NPM package and use the files from `build/viewer` diractory
|
||||
- Use `jsdelivr` CDN
|
||||
- `<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.js" />`
|
||||
- `<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/molstar@latest/build/viewer/molstar.css" />`
|
||||
- `@latest` can be replaced by a specific Mol* version, e.g., `@5.4.2`
|
||||
- Clone & build the GitHub repository
|
||||
- This option allows for quite straightforward extension customization, e.g., not including movie export, which reduces the bundle size by ~0.5MB
|
||||
|
||||
### Bundle size
|
||||
|
||||
By default, the `Viewer` includes all the available extensions. This increases the bundle size significantly, especially by including the `mp4-export`, which is responsible for almost `0.5MB` of compressed bundle size.
|
||||
It is quite easy to reduce this bundle size by cloning the Mol\* repository, editing [extensions.ts](https://github.com/molstar/molstar/blob/master/src/apps/viewer/options.ts) and rebuilding it with `npm run build:apps`. The new build will be available
|
||||
in the `build/viewer` directory (the JS file you will find there is uncompressed, but your hosting setup should include automatic gzip compression, significantly reducing the size).
|
||||
|
||||
Alternatively, you can explore building your own "viewer" using the base Mol\* library. For this, see the options below.
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
```HTML
|
||||
<style>
|
||||
@@ -35,7 +58,7 @@ Example usage without using WebPack:
|
||||
- the folder build/viewer after cloning and building the molstar package
|
||||
- from the build/viewer folder in the Mol* NPM package
|
||||
-->
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<link rel="stylesheet" type="text/css" href="./molstar.css" />
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
|
||||
<div id="app"></div>
|
||||
@@ -62,13 +85,15 @@ Example usage without using WebPack:
|
||||
</script>
|
||||
```
|
||||
|
||||
When using WebPack (or possibly other build tool) with the Mol* NPM package installed, the viewer class can be imported using
|
||||
### Using WebPack/esbuild/...
|
||||
|
||||
When using WebPack (or other bundler) with the Mol* NPM package installed, the viewer class can be imported using
|
||||
|
||||
```ts
|
||||
import { Viewer } from 'molstar/build/viewer/molstar'
|
||||
import { Viewer } from 'molstar/lib/apps/viewer/app'
|
||||
|
||||
function initViewer(target: string | HTMLElement) {
|
||||
return new Viewer(target, { /* options */})
|
||||
return Viewer.create(target, { /* options */}) // returns a Promise
|
||||
}
|
||||
```
|
||||
|
||||
@@ -139,6 +164,8 @@ export function MolStarWrapper() {
|
||||
// In debug mode of react's strict mode, this code will
|
||||
// be called twice in a row, which might result in unexpected behavior.
|
||||
useEffect(() => {
|
||||
// By default, react will call each useEffect twice if using Strict mode in
|
||||
// debug build, it is recommended to disable strict mode for this reason if possible
|
||||
async function init() {
|
||||
window.molstar = await createPluginUI({
|
||||
target: parent.current as HTMLDivElement,
|
||||
|
||||
@@ -1,59 +1,44 @@
|
||||
# Selections
|
||||
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the following method you can select structural data.
|
||||
## Basic Concepts
|
||||
|
||||
### Selecting directly from the `hierarchy` manager
|
||||
### Location
|
||||
|
||||
One can select a subcomponent's data directly from the plugin manager.
|
||||
The selection model in Mol\* is based on a generic concept called *location*. A location is a pointer to a selectable element within a scene. For example:
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
- A structure element location (an atom or a coarse element) is an object composed of `{ structure: Structure, unit: Unit, element: UnitIndex }` (you can think of a `Unit` as a generalized chain)
|
||||
- A bond location is very similar to structure element, requiring pointers to two units and elements
|
||||
- A "shape" (generally a mesh) location consists of pointer to the parent shape and a group of triangles
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
### Loci
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
Structures and other renderable elements generally consist of many locations and simply using a list of locations would be
|
||||
prohibitively expensive (e.g., large selections in structures with hundreds of thousands of atoms).
|
||||
|
||||
## Selection callbacks
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
This is why Mol\* introduces
|
||||
the concept of `Loci` — a compressed representation of multiple locations. Instead of having a list of structure element locations (`{ structure: Structure, unit: Unit, element: UnitIndex }[]`), the representation becomes (simplified) `{ structure: Structure, unit: Unit, elements: OrderedSet<UnitIndex> }`. The ordered set can be further compressed for continuous ranges, keeping only the index of the 1st and last element.
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
### Bundle
|
||||
|
||||
### `Molscript` language
|
||||
Locations and loci point to the raw JavaScript data structures representing the underlying molecules, making them not serializable in JSON. A *bundle* is a serializable version of the loci.
|
||||
|
||||
Molscript is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
### Structure Queries
|
||||
|
||||
### Querying a structure for a specific chain and residue range (select residues with 12<res_id<200 of chain with auth_asym_id==A) :
|
||||
Defining selections directly using the loci would be very cumbersome. For this reason, Mol\* includes the [MolQl query language](https://molql.org) to help define selections.
|
||||
|
||||
|
||||
## Selection Methods
|
||||
|
||||
Assuming you have a model already loaded into the plugin (see [Creating Plugin Instance](./instance.md)), these are some of the methods you can use to create selections.
|
||||
|
||||
### MolQL (`mol-script`) language
|
||||
|
||||
[MolQL](https://molql.org) (`mol-script`) is a language for addressing crystallographic structures and is a part of the Mol* library found at `https://github.com/molstar/molstar/tree/master/src/mol-script`. It can be used against the Molstar plugin as a query language and transpiled against multiple external molecular visualization libraries(see [here](https://github.com/molstar/molstar/tree/master/src/mol-script/transpilers)).
|
||||
|
||||
**Example:** Querying a structure for a specific chain and residue range
|
||||
|
||||
Select residues with `12<res_id<200 of chain with auth_asym_id=A`
|
||||
|
||||
```typescript
|
||||
import { compileIdListSelection } from 'molstar/lib/mol-script/util/id-list'
|
||||
@@ -62,12 +47,12 @@ const query = compileIdListSelection('A 12-200', 'auth');
|
||||
window.molstar?.managers.structure.selection.fromCompiledQuery('add',query);
|
||||
```
|
||||
|
||||
## Selection Queries
|
||||
### Selection Queries
|
||||
|
||||
Another way to create a selection is via a `SelectionQuery` object. This is a more programmatic way to create a selection. The following example shows how to select a chain and a residue range using a `SelectionQuery` object.
|
||||
This relies on the concept of `Expression` which is basically a intermediate representation between a Molscript statement and a selection query.
|
||||
|
||||
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
|
||||
**Example:** Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object
|
||||
|
||||
```typescript
|
||||
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
|
||||
@@ -107,7 +92,7 @@ var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
let loci = StructureSelection.toLociWithSourceUnits(sel);
|
||||
```
|
||||
|
||||
## Query Functions
|
||||
### Query Functions
|
||||
|
||||
Instead of building expressions, query functions can be created directly, e.g.:
|
||||
|
||||
@@ -125,7 +110,7 @@ const selection = query(new QueryContext(structure));
|
||||
// ...
|
||||
```
|
||||
|
||||
## Selection Schema
|
||||
### Selection Schema
|
||||
|
||||
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
|
||||
|
||||
@@ -143,6 +128,63 @@ const loci = StructureElement.Loci.fromSchema(structure, residues);
|
||||
|
||||
Usually, a code editor such as VS Code will auto-suggest all the available field names.
|
||||
|
||||
### Using the `hierarchy` manager
|
||||
|
||||
It is possible to select a subcomponent's data directly from the plugin manager.
|
||||
|
||||
```typescript
|
||||
import { Structure } from '../mol-model/structure';
|
||||
|
||||
const ligandData = plugin.managers.structure.hierarchy.selection.structures[0]?.components[0]?.cell.obj?.data;
|
||||
const ligandLoci = Structure.toStructureElementLoci(ligandData as any);
|
||||
|
||||
plugin.managers.camera.focusLoci(ligandLoci);
|
||||
plugin.managers.interactivity.lociSelects.select({ loci: ligandLoci });
|
||||
```
|
||||
|
||||
## Selection Events
|
||||
If you want to subscribe to selection events (e.g. to change external state in your application based on a user selection), you can use: `plugin.behaviors.interaction.click.subscribe`
|
||||
|
||||
Here's an example of passing in a React "set" function to update selected residue positions.
|
||||
```typescript
|
||||
import {
|
||||
Structure,
|
||||
StructureProperties,
|
||||
} from "molstar/lib/mol-model/structure"
|
||||
// setSelected is assumed to be a "set" function returned by useState
|
||||
// (selected: any[]) => void
|
||||
plugin.behaviors.interaction.click.subscribe(
|
||||
(event: InteractivityManager.ClickEvent) => {
|
||||
const selections = Array.from(
|
||||
plugin.managers.structure.selection.entries.values()
|
||||
);
|
||||
// This bit can be customized to record any piece information you want
|
||||
const localSelected: any[] = [];
|
||||
for (const { structure } of selections) {
|
||||
if (!structure) continue;
|
||||
Structure.eachAtomicHierarchyElement(structure, {
|
||||
residue: (loc) => {
|
||||
const position = StructureProperties.residue.label_seq_id(loc);
|
||||
localSelected.push({ position });
|
||||
},
|
||||
});
|
||||
}
|
||||
setSelected(localSelected);
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
|
||||
### `Viewer` app
|
||||
|
||||
The `Viewer` app provides the `structureInteractivity` function which allows easy selection/highlighting of the loaded structure. For example:
|
||||
|
||||
```ts
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 10, end_auth_seq_id: 50 },
|
||||
action: 'select',
|
||||
});
|
||||
```
|
||||
152
docs/docs/plugin/superposition.md
Normal file
152
docs/docs/plugin/superposition.md
Normal file
@@ -0,0 +1,152 @@
|
||||
# Structure Superposition
|
||||
|
||||
Mol* provides utilities for superposing protein structures, including both sequence-independent (RMSD-based) and structure-based (TM-align) methods.
|
||||
|
||||
## RMSD-based Superposition
|
||||
|
||||
The basic superposition method uses the Kabsch algorithm to minimize RMSD between corresponding atoms:
|
||||
|
||||
```typescript
|
||||
import { superpose } from 'molstar/lib/mol-model/structure/structure/util/superposition';
|
||||
import { StructureSelection, QueryContext } from 'molstar/lib/mol-model/structure';
|
||||
import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
|
||||
import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
|
||||
|
||||
// Create a query for C-alpha atoms
|
||||
const caQuery = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
|
||||
// Get selections from two structures
|
||||
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(structure1)));
|
||||
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(structure2)));
|
||||
|
||||
// Compute superposition (returns transformation matrices)
|
||||
const transforms = superpose([sel1, sel2]);
|
||||
|
||||
// transforms[0].bTransform contains the Mat4 to superpose structure2 onto structure1
|
||||
```
|
||||
|
||||
## TM-align Superposition
|
||||
|
||||
TM-align is a structure-based alignment algorithm that produces the TM-score, a length-independent metric for comparing protein structures. Unlike RMSD, TM-score is normalized to [0, 1] and is more robust for comparing proteins of different sizes.
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```typescript
|
||||
import { tmAlign } from 'molstar/lib/mol-model/structure/structure/util/tm-align';
|
||||
import { StructureElement } from 'molstar/lib/mol-model/structure';
|
||||
|
||||
// Get C-alpha Loci from two structures (see selection examples above)
|
||||
const loci1: StructureElement.Loci = /* ... */;
|
||||
const loci2: StructureElement.Loci = /* ... */;
|
||||
|
||||
// Run TM-align
|
||||
const result = tmAlign(loci1, loci2);
|
||||
|
||||
console.log('TM-score (normalized by structure 1):', result.tmScoreA);
|
||||
console.log('TM-score (normalized by structure 2):', result.tmScoreB);
|
||||
console.log('RMSD:', result.rmsd);
|
||||
console.log('Aligned residues:', result.alignedLength);
|
||||
|
||||
// result.bTransform is a Mat4 to transform structure2 onto structure1
|
||||
```
|
||||
|
||||
### TM-align Result
|
||||
|
||||
The `tmAlign` function returns a `TMAlignResult` object with the following properties:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `bTransform` | `Mat4` | Transformation matrix to superpose structure B onto A |
|
||||
| `tmScoreA` | `number` | TM-score normalized by length of structure A |
|
||||
| `tmScoreB` | `number` | TM-score normalized by length of structure B |
|
||||
| `rmsd` | `number` | RMSD of aligned residue pairs (in Angstroms) |
|
||||
| `alignedLength` | `number` | Number of aligned residue pairs |
|
||||
| `sequenceIdentity` | `number` | Sequence identity of aligned residues (0-1) |
|
||||
| `alignmentA` | `number[]` | Indices of aligned residues in structure A |
|
||||
| `alignmentB` | `number[]` | Indices of aligned residues in structure B |
|
||||
|
||||
### Understanding TM-score
|
||||
|
||||
The TM-score is calculated as:
|
||||
|
||||
$$\text{TM-score} = \frac{1}{L} \sum_{i=1}^{L_{ali}} \frac{1}{1 + (d_i/d_0)^2}$$
|
||||
|
||||
Where:
|
||||
- $L$ is the length of the reference protein
|
||||
- $L_{ali}$ is the number of aligned residues
|
||||
- $d_i$ is the distance between the $i$-th pair of aligned residues after superposition
|
||||
- $d_0 = 1.24 \sqrt[3]{L - 15} - 1.8$ is a length-dependent normalization factor
|
||||
|
||||
**TM-score interpretation:**
|
||||
- TM-score > 0.5: Generally indicates proteins with the same fold
|
||||
- TM-score > 0.17: Generally indicates proteins with random structural similarity
|
||||
|
||||
### Low-level API
|
||||
|
||||
For direct coordinate-based alignment without structures, use the `TMAlign` namespace:
|
||||
|
||||
```typescript
|
||||
import { TMAlign } from 'molstar/lib/mol-math/linear-algebra/3d/tm-align';
|
||||
|
||||
// Create position arrays
|
||||
const posA = TMAlign.Positions.empty(lengthA);
|
||||
const posB = TMAlign.Positions.empty(lengthB);
|
||||
|
||||
// Fill in coordinates
|
||||
for (let i = 0; i < lengthA; i++) {
|
||||
posA.x[i] = /* x coordinate */;
|
||||
posA.y[i] = /* y coordinate */;
|
||||
posA.z[i] = /* z coordinate */;
|
||||
}
|
||||
// ... similarly for posB
|
||||
|
||||
// Compute alignment
|
||||
const result = TMAlign.compute({ a: posA, b: posB });
|
||||
```
|
||||
|
||||
### Complete Example: Aligning Two PDB Structures
|
||||
|
||||
```typescript
|
||||
import { PluginContext } from 'molstar/lib/mol-plugin/context';
|
||||
import { MolScriptBuilder as MS } from 'molstar/lib/mol-script/language/builder';
|
||||
import { compile } from 'molstar/lib/mol-script/runtime/query/compiler';
|
||||
import { StructureSelection, QueryContext, StructureElement } from 'molstar/lib/mol-model/structure';
|
||||
import { tmAlign } from 'molstar/lib/mol-model/structure/structure/util/tm-align';
|
||||
import { StateTransforms } from 'molstar/lib/mol-plugin-state/transforms';
|
||||
import { Mat4 } from 'molstar/lib/mol-math/linear-algebra';
|
||||
|
||||
async function alignStructures(plugin: PluginContext, structure1: any, structure2: any) {
|
||||
// Query for C-alpha atoms in chain A
|
||||
const caQuery = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), 'A']),
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
|
||||
// Get structure data
|
||||
const data1 = structure1.cell?.obj?.data;
|
||||
const data2 = structure2.cell?.obj?.data;
|
||||
|
||||
// Create selections
|
||||
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(data1)));
|
||||
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery(new QueryContext(data2)));
|
||||
|
||||
// Run TM-align
|
||||
const result = tmAlign(sel1, sel2);
|
||||
|
||||
// Apply transformation to structure2
|
||||
const b = plugin.state.data.build().to(structure2)
|
||||
.insert(StateTransforms.Model.TransformStructureConformation, {
|
||||
transform: { name: 'matrix', params: { data: result.bTransform, transpose: false } }
|
||||
});
|
||||
await plugin.runTask(plugin.state.data.updateTree(b));
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- Zhang Y, Skolnick J. "TM-align: a protein structure alignment algorithm based on the TM-score." *Nucleic Acids Research* 33, 2302-2309 (2005). DOI: [10.1093/nar/gki524](https://doi.org/10.1093/nar/gki524)
|
||||
- Kabsch W. "A solution for the best rotation to relate two sets of vectors." *Acta Crystallographica* A32, 922-923 (1976).
|
||||
@@ -33,6 +33,7 @@ nav:
|
||||
- Examples: plugin/examples.md
|
||||
- Custom Library: 'plugin/custom-library.md'
|
||||
- Selections: 'plugin/selections.md'
|
||||
- Superposition: 'plugin/superposition.md'
|
||||
- Viewer State: 'plugin/viewer-state.md'
|
||||
- Data State: 'plugin/data-state.md'
|
||||
- File Formats: 'plugin/file-formats.md'
|
||||
|
||||
BIN
examples/trajectory/protein.dcd
Normal file
BIN
examples/trajectory/protein.dcd
Normal file
Binary file not shown.
BIN
examples/trajectory/protein.nc
Normal file
BIN
examples/trajectory/protein.nc
Normal file
Binary file not shown.
264
examples/trajectory/protein.parm7
Normal file
264
examples/trajectory/protein.parm7
Normal file
@@ -0,0 +1,264 @@
|
||||
%VERSION VERSION_STAMP = V0001.000 DATE = 11/04/25 11:55:47
|
||||
%FLAG TITLE
|
||||
%FORMAT(20a4)
|
||||
alanine-dipeptide.solvated.pdb
|
||||
%FLAG POINTERS
|
||||
%FORMAT(10I8)
|
||||
22 7 12 9 25 11 39 19 0 0
|
||||
99 3 9 11 19 7 11 20 0 0
|
||||
0 0 0 0 0 0 0 1 10 0
|
||||
0 1
|
||||
%FLAG ATOM_NAME
|
||||
%FORMAT(20a4)
|
||||
H1 CH3 H2 H3 C O N H CA HA CB HB1 HB2 HB3 C O N H C H1
|
||||
H2 H3
|
||||
%FLAG ATOMIC_NUMBER
|
||||
%FORMAT(10I8)
|
||||
1 6 1 1 6 8 7 1 6 1
|
||||
6 1 1 1 6 8 7 1 6 1
|
||||
1 1
|
||||
%FLAG RESIDUE_LABEL
|
||||
%FORMAT(20a4)
|
||||
ACE ALA NME
|
||||
%FLAG RESIDUE_POINTER
|
||||
%FORMAT(10I8)
|
||||
1 7 17
|
||||
%FLAG RESIDUE_NUMBER
|
||||
%FORMAT(20I4)
|
||||
1 2 3
|
||||
%FLAG RESIDUE_ICODE
|
||||
%FORMAT(20a4)
|
||||
|
||||
%FLAG RESIDUE_CHAINID
|
||||
%FORMAT(20a4)
|
||||
B B B
|
||||
%FLAG SOLVENT_POINTERS
|
||||
%FORMAT(3I8)
|
||||
0 1 0
|
||||
%FLAG ATOMS_PER_MOLECULE
|
||||
%FORMAT(10I8)
|
||||
22
|
||||
%FLAG MASS
|
||||
%FORMAT(5E16.8)
|
||||
3.02400000E+00 5.96200000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 9.99400000E+00 3.02400000E+00
|
||||
5.96200000E+00 3.02400000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 5.96200000E+00 3.02400000E+00
|
||||
3.02400000E+00 3.02400000E+00
|
||||
%FLAG CHARGE
|
||||
%FORMAT(5E16.8)
|
||||
2.04636429E+00 -6.67300626E+00 2.04636429E+00 2.04636429E+00 1.08823576E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 6.14091510E-01 1.49969529E+00
|
||||
-3.32556975E+00 1.09880469E+00 1.09880469E+00 1.09880469E+00 1.08841798E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 -2.71512270E+00 1.77849648E+00
|
||||
1.77849648E+00 1.77849648E+00
|
||||
%FLAG AMBER_ATOM_TYPE
|
||||
%FORMAT(20a4)
|
||||
a0 a1 a0 a0 a2 a3 a4 a5 a1 a6 a1 a0 a0 a0 a2 a3 a4 a5 a1 a6
|
||||
a6 a6
|
||||
%FLAG ATOM_TYPE_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 1 1 3 4 5 6 2 7
|
||||
2 1 1 1 3 4 5 6 2 7
|
||||
7 7
|
||||
%FLAG NONBONDED_PARM_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 4 7 11 16 22 2 3 5
|
||||
8 12 17 23 4 5 6 9 13 18
|
||||
24 7 8 9 10 14 19 25 11 12
|
||||
13 14 15 20 26 16 17 18 19 20
|
||||
21 27 22 23 24 25 26 27 28
|
||||
%FLAG LENNARD_JONES_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
7.51607703E+03 9.71708117E+04 1.04308023E+06 8.61541883E+04 9.24822269E+05
|
||||
8.19971662E+05 5.44261042E+04 6.47841732E+05 5.74393458E+05 3.79876399E+05
|
||||
8.96776989E+04 9.95480466E+05 8.82619071E+05 6.06829343E+05 9.44293233E+05
|
||||
1.07193645E+02 2.56678134E+03 2.27577560E+03 1.02595236E+03 2.12601181E+03
|
||||
1.39982777E-01 4.98586847E+03 6.78771368E+04 6.01816484E+04 3.69471530E+04
|
||||
6.20665998E+04 5.94667299E+01 3.25969625E+03
|
||||
%FLAG LENNARD_JONES_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
2.17257828E+01 1.26919150E+02 6.75612247E+02 1.12529845E+02 5.99015525E+02
|
||||
5.31102864E+02 1.11805549E+02 6.26720080E+02 5.55666449E+02 5.64885984E+02
|
||||
1.36131731E+02 7.36907417E+02 6.53361429E+02 6.77220874E+02 8.01323529E+02
|
||||
2.59456373E+00 2.06278363E+01 1.82891803E+01 1.53505284E+01 2.09604198E+01
|
||||
9.37598976E-02 1.76949863E+01 1.06076943E+02 9.40505981E+01 9.21192137E+01
|
||||
1.13252062E+02 1.93248820E+00 1.43076527E+01
|
||||
%FLAG NUMBER_EXCLUDED_ATOMS
|
||||
%FORMAT(10I8)
|
||||
6 7 4 3 7 3 10 4 10 7
|
||||
6 3 2 1 7 3 5 4 3 2
|
||||
1 1
|
||||
%FLAG EXCLUDED_ATOMS_LIST
|
||||
%FORMAT(10I8)
|
||||
2 3 4 5 6 7 3 4 5 6
|
||||
7 8 9 4 5 6 7 5 6 7
|
||||
6 7 8 9 10 11 15 7 8 9
|
||||
8 9 10 11 12 13 14 15 16 17
|
||||
9 10 11 15 10 11 12 13 14 15
|
||||
16 17 18 19 11 12 13 14 15 16
|
||||
17 12 13 14 15 16 17 13 14 15
|
||||
14 15 15 16 17 18 19 20 21 22
|
||||
17 18 19 18 19 20 21 22 19 20
|
||||
21 22 20 21 22 21 22 22 0
|
||||
%FLAG BOND_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.40000000E+02 4.34000000E+02 3.17000000E+02 5.70000000E+02 4.90000000E+02
|
||||
3.37000000E+02 3.10000000E+02
|
||||
%FLAG BOND_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.09000000E+00 1.01000000E+00 1.52200000E+00 1.22900000E+00 1.33500000E+00
|
||||
1.44900000E+00 1.52600000E+00
|
||||
%FLAG BONDS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 1 3 6 1 3 9 1 18
|
||||
21 2 24 27 1 30 33 1 30 36
|
||||
1 30 39 1 48 51 2 54 57 1
|
||||
54 60 1 54 63 1
|
||||
%FLAG BONDS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 3 12 15 4 12 18 5 18
|
||||
24 6 24 42 3 24 30 7 42 48
|
||||
5 42 45 4 48 54 6
|
||||
%FLAG ANGLE_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.50000000E+01 5.00000000E+01 5.00000000E+01 5.00000000E+01 8.00000000E+01
|
||||
7.00000000E+01 5.00000000E+01 8.00000000E+01 8.00000000E+01 6.30000000E+01
|
||||
6.30000000E+01
|
||||
%FLAG ANGLE_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.91113553E+00 1.91113553E+00 2.09439510E+00 2.06018665E+00 2.10137642E+00
|
||||
2.03505391E+00 2.12755636E+00 2.14500965E+00 1.91462619E+00 1.92160751E+00
|
||||
1.93906080E+00
|
||||
%FLAG ANGLES_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 6 1 0 3 9 1 0 3
|
||||
12 2 6 3 9 1 6 3 12 2
|
||||
9 3 12 2 12 18 21 3 18 24
|
||||
27 2 21 18 24 4 24 30 33 2
|
||||
24 30 36 2 24 30 39 2 27 24
|
||||
30 2 27 24 42 2 33 30 36 1
|
||||
33 30 39 1 36 30 39 1 42 48
|
||||
51 3 48 54 57 2 48 54 60 2
|
||||
48 54 63 2 51 48 54 4 57 54
|
||||
60 1 57 54 63 1 60 54 63 1
|
||||
%FLAG ANGLES_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 15 5 3 12 18 6 12 18
|
||||
24 7 15 12 18 8 18 24 30 9
|
||||
18 24 42 10 24 42 45 5 24 42
|
||||
48 6 30 24 42 11 42 48 54 7
|
||||
45 42 48 8
|
||||
%FLAG DIHEDRAL_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
8.00000000E-01 8.00000000E-02 2.50000000E+00 2.50000000E+00 2.00000000E+00
|
||||
1.55555556E-01 1.10000000E+00 0.00000000E+00 0.00000000E+00 8.00000000E-01
|
||||
1.80000000E+00 4.20000000E-01 2.70000000E-01 5.50000000E-01 1.58000000E+00
|
||||
4.50000000E-01 4.00000000E-01 2.00000000E-01 2.00000000E-01 1.05000000E+01
|
||||
%FLAG DIHEDRAL_PERIODICITY
|
||||
%FORMAT(5E16.8)
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 2.00000000E+00 1.00000000E+00
|
||||
3.00000000E+00 2.00000000E+00 1.00000000E+00 1.00000000E+00 3.00000000E+00
|
||||
2.00000000E+00 3.00000000E+00 2.00000000E+00 3.00000000E+00 2.00000000E+00
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 1.00000000E+00 2.00000000E+00
|
||||
%FLAG DIHEDRAL_PHASE
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 3.14159265E+00 3.14159265E+00 3.14159265E+00 0.00000000E+00
|
||||
0.00000000E+00 3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00 3.14159265E+00
|
||||
3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00
|
||||
%FLAG SCEE_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 0.00000000E+00
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 1.20000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 1.20000000E+00 0.00000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCNB_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 2.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG DIHEDRALS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 12 15 1 0 3 -12 15 2
|
||||
3 12 18 21 3 6 3 12 15 1
|
||||
6 3 -12 15 2 9 3 12 15 1
|
||||
9 3 -12 15 2 15 12 18 21 4
|
||||
15 12 -18 21 5 18 24 30 33 6
|
||||
18 24 30 36 6 18 24 30 39 6
|
||||
24 42 48 51 3 27 24 30 33 6
|
||||
27 24 30 36 6 27 24 30 39 6
|
||||
27 24 42 45 1 27 24 -42 45 2
|
||||
42 24 30 33 6 42 24 30 36 6
|
||||
42 24 30 39 6 45 42 48 51 4
|
||||
45 42 -48 51 5 21 18 -24 -12 7
|
||||
51 48 -54 -42 7 51 48 54 60 8
|
||||
21 18 24 30 8 42 48 54 57 8
|
||||
6 3 12 18 9 42 48 54 63 8
|
||||
51 48 54 57 8 21 18 24 42 8
|
||||
0 3 12 18 9 42 48 54 60 8
|
||||
27 24 42 48 8 21 18 24 27 8
|
||||
51 48 54 63 8 9 3 12 18 9
|
||||
12 18 24 27 8
|
||||
%FLAG DIHEDRALS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 18 24 3 12 18 24 30 10
|
||||
12 18 -24 30 11 12 18 -24 30 5
|
||||
12 18 24 42 12 12 18 -24 42 13
|
||||
15 12 18 24 3 18 24 42 48 14
|
||||
18 24 -42 48 15 18 24 -42 48 16
|
||||
24 42 48 54 3 30 24 42 48 17
|
||||
30 24 -42 48 18 30 24 -42 48 19
|
||||
45 42 48 54 3 15 12 -18 -3 20
|
||||
45 42 -48 -24 20 18 24 42 45 8
|
||||
30 24 42 45 8
|
||||
%FLAG SOLTY
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBCUT
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG TREE_CHAIN_CLASSIFICATION
|
||||
%FORMAT(20a4)
|
||||
BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA
|
||||
BLA BLA
|
||||
%FLAG JOIN_ARRAY
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG IROTAT
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG BOX_DIMENSIONS
|
||||
%FORMAT(5E16.8)
|
||||
9.00000000E+01 3.00000000E+01 3.00000000E+01 3.00000000E+01
|
||||
%FLAG RADIUS_SET
|
||||
%FORMAT(1a80)
|
||||
0
|
||||
%FLAG RADII
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCREEN
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG IPOL
|
||||
%FORMAT(1I8)
|
||||
0
|
||||
26
examples/trajectory/protein.pdb
Normal file
26
examples/trajectory/protein.pdb
Normal file
@@ -0,0 +1,26 @@
|
||||
CRYST1 30.000 30.000 30.000 90.00 90.00 90.00 P 1 1
|
||||
ATOM 1 H1 ACE A 1 2.000 1.000 -0.000 0.00 0.00 H
|
||||
ATOM 2 CH3 ACE A 1 2.000 2.090 0.000 0.00 0.00 C
|
||||
ATOM 3 H2 ACE A 1 1.486 2.454 0.890 0.00 0.00 H
|
||||
ATOM 4 H3 ACE A 1 1.486 2.454 -0.890 0.00 0.00 H
|
||||
ATOM 5 C ACE A 1 3.427 2.641 -0.000 0.00 0.00 C
|
||||
ATOM 6 O ACE A 1 4.391 1.877 -0.000 0.00 0.00 O
|
||||
ATOM 7 N ALA A 2 3.555 3.970 -0.000 0.00 0.00 N
|
||||
ATOM 8 H ALA A 2 2.733 4.556 -0.000 0.00 0.00 H
|
||||
ATOM 9 CA ALA A 2 4.853 4.614 -0.000 0.00 0.00 C
|
||||
ATOM 10 HA ALA A 2 5.408 4.316 0.890 0.00 0.00 H
|
||||
ATOM 11 CB ALA A 2 5.661 4.221 -1.232 0.00 0.00 C
|
||||
ATOM 12 HB1 ALA A 2 5.123 4.521 -2.131 0.00 0.00 H
|
||||
ATOM 13 HB2 ALA A 2 6.630 4.719 -1.206 0.00 0.00 H
|
||||
ATOM 14 HB3 ALA A 2 5.809 3.141 -1.241 0.00 0.00 H
|
||||
ATOM 15 C ALA A 2 4.713 6.129 0.000 0.00 0.00 C
|
||||
ATOM 16 O ALA A 2 3.601 6.653 0.000 0.00 0.00 O
|
||||
ATOM 17 N NME A 3 5.846 6.835 0.000 0.00 0.00 N
|
||||
ATOM 18 H NME A 3 6.737 6.359 -0.000 0.00 0.00 H
|
||||
ATOM 19 C NME A 3 5.846 8.284 0.000 0.00 0.00 C
|
||||
ATOM 20 H1 NME A 3 4.819 8.648 0.000 0.00 0.00 H
|
||||
ATOM 21 H2 NME A 3 6.360 8.648 0.890 0.00 0.00 H
|
||||
ATOM 22 H3 NME A 3 6.360 8.648 -0.890 0.00 0.00 H
|
||||
TER 23 NME A 3
|
||||
CONECT 5 7
|
||||
CONECT 15 17
|
||||
14
examples/trajectory/protein.rst7
Normal file
14
examples/trajectory/protein.rst7
Normal file
@@ -0,0 +1,14 @@
|
||||
alanine-dipeptide.solvated.pdb
|
||||
22
|
||||
0.7494821 1.2436848 0.8743532 1.0856344 2.2423820 0.5955986
|
||||
0.4304414 2.9747953 1.0671825 1.0497815 2.3544810 -0.4880289
|
||||
2.5015950 2.4471725 1.0820421 3.1003812 1.5343071 1.6479120
|
||||
3.0220696 3.6519467 0.8741013 2.4411554 4.3533213 0.4373955
|
||||
4.3920715 4.0500473 1.2160543 4.7674596 3.4172266 2.0202454
|
||||
5.2805058 3.8202998 -0.0180103 4.9565949 4.4537317 -0.8438106
|
||||
6.3180425 4.0583459 0.2164072 5.2327259 2.7740601 -0.3200050
|
||||
4.4431625 5.5106563 1.7135265 3.4307644 6.2198007 1.6891606
|
||||
5.6170320 5.9613562 2.1744082 6.3997462 5.3231585 2.1616313
|
||||
5.8784762 7.3296314 2.6320299 5.1056278 8.0184146 2.2908769
|
||||
5.9253575 7.3544224 3.7207393 6.8360338 7.6745804 2.2419090
|
||||
30.0000000 30.0000000 30.0000000 90.0000000 90.0000000 90.0000000
|
||||
8571
package-lock.json
generated
8571
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
59
package.json
59
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.2.0",
|
||||
"version": "5.9.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -11,7 +11,7 @@
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
@@ -74,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
@@ -123,7 +123,10 @@
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>",
|
||||
"Tianzhen Lin (Tangent) <tangent@usa.net>",
|
||||
"Russ Taylor <russ@reliasolve.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -131,53 +134,51 @@
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.24",
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/webxr": "^0.5.23",
|
||||
"@typescript-eslint/eslint-plugin": "^8.44.1",
|
||||
"@typescript-eslint/parser": "^8.44.1",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"cpx2": "^8.0.0",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild": "^0.25.10",
|
||||
"cpx2": "^8.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "^9.36.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"esbuild-sass-plugin": "^3.7.0",
|
||||
"eslint": "^10.3.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"globals": "^17.6.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.93.2",
|
||||
"simple-git": "^3.28.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.2"
|
||||
"sass": "^1.99.0",
|
||||
"simple-git": "^3.36.0",
|
||||
"tsc-alias": "^1.8.17",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^20.19.17",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.19.17",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immutable": "^5.1.3",
|
||||
"immutable": "^5.1.5",
|
||||
"io-ts": "^2.2.22",
|
||||
"mutative": "^1.3.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.29.0",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.3"
|
||||
"swagger-ui-dist": "^5.32.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
|
||||
@@ -13,7 +13,7 @@ import * as os from 'os';
|
||||
|
||||
const Apps = [
|
||||
// Apps
|
||||
{ kind: 'app', name: 'viewer' },
|
||||
{ kind: 'app', name: 'viewer', themes: ['light', 'dark', 'blue'] },
|
||||
{ kind: 'app', name: 'docking-viewer' },
|
||||
{ kind: 'app', name: 'mesoscale-explorer' },
|
||||
{ kind: 'app', name: 'mvs-stories', globalName: 'mvsStories', filename: 'mvs-stories.js' },
|
||||
@@ -132,7 +132,6 @@ function getPaths(app) {
|
||||
async function createBundle(app) {
|
||||
const { name, kind } = app;
|
||||
const { prefix, entry, outfile } = getPaths(app);
|
||||
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [entry],
|
||||
@@ -173,6 +172,41 @@ async function createBundle(app) {
|
||||
if (!isProduction) await ctx.watch();
|
||||
}
|
||||
|
||||
async function createTheme(appName, themeName) {
|
||||
// const { prefix, entry, outfile } = getPaths(app);
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [resolveEntryPath(`./src/apps/${appName}/theme/${themeName}.ts`)],
|
||||
tsconfig: './tsconfig.json',
|
||||
bundle: true,
|
||||
minify: isProduction,
|
||||
minifyIdentifiers: false,
|
||||
sourcemap: false,
|
||||
outfile: `./build/${appName}/theme/${themeName}.js`,
|
||||
plugins: [
|
||||
// fileLoaderPlugin({ out: prefix }),
|
||||
sassPlugin({
|
||||
type: 'css',
|
||||
silenceDeprecations: ['import'],
|
||||
logger: {
|
||||
warn: (msg) => console.warn(msg),
|
||||
debug: () => { },
|
||||
}
|
||||
}),
|
||||
],
|
||||
color: true,
|
||||
logLevel: 'info',
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
|
||||
if (!isProduction) await ctx.watch();
|
||||
}
|
||||
|
||||
function findBrowserTests(names) {
|
||||
const dir = path.resolve('./src', 'tests', 'browser');
|
||||
let files = fs.readdirSync(dir).filter(file => file.endsWith('.ts')).map(file => file.replace('.ts', ''));
|
||||
@@ -230,6 +264,7 @@ const args = argParser.parse_args();
|
||||
const isProduction = !!args.prd;
|
||||
const includeSourceMap = !args.no_src_map;
|
||||
|
||||
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
|
||||
const VERSION = isProduction ? JSON.parse(fs.readFileSync('./package.json', 'utf8')).version : '(dev build)';
|
||||
const TIMESTAMP = Date.now();
|
||||
|
||||
@@ -261,7 +296,14 @@ async function main() {
|
||||
const promises = [];
|
||||
console.log(isProduction ? 'Building apps...' : 'Initial build...');
|
||||
|
||||
for (const app of apps) promises.push(createBundle(app));
|
||||
for (const app of apps) {
|
||||
promises.push(createBundle(app));
|
||||
if (app.themes) {
|
||||
for (const theme of app.themes) {
|
||||
promises.push(createTheme(app.name, theme));
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const example of examples) promises.push(createBundle(example));
|
||||
for (const browserTest of browserTests) promises.push(createBundle(browserTest));
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -36,6 +36,12 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
|
||||
approximate: gmp.approximate,
|
||||
alphaThickness: gmp.alphaThickness,
|
||||
visuals: [merge ? 'structure-element-sphere' : 'element-sphere'],
|
||||
interior: {
|
||||
color: Color.fromNormalizedRgb(0, 0, 0),
|
||||
colorStrength: 0.15,
|
||||
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
|
||||
substanceStrength: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
colorTheme: {
|
||||
@@ -49,7 +55,7 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: 1,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -50,6 +50,12 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
|
||||
clipPrimitive: true,
|
||||
approximate: gmp.approximate,
|
||||
alphaThickness: gmp.alphaThickness,
|
||||
interior: {
|
||||
color: Color.fromNormalizedRgb(0, 0, 0),
|
||||
colorStrength: 0.15,
|
||||
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
|
||||
substanceStrength: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
colorTheme: {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../../../mol-model/structure/model/model';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
@@ -40,6 +41,12 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
|
||||
clipPrimitive: true,
|
||||
approximate: gmp.approximate,
|
||||
alphaThickness: gmp.alphaThickness,
|
||||
interior: {
|
||||
color: Color.fromNormalizedRgb(0, 0, 0),
|
||||
colorStrength: 0.15,
|
||||
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
|
||||
substanceStrength: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
colorTheme: {
|
||||
@@ -53,7 +60,7 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: scaleFactor,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -96,6 +103,8 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
});
|
||||
}
|
||||
|
||||
const coarseGrained = Model.isCoarseGrained(model.data!);
|
||||
|
||||
const entGroups = new Map<string, StateObjectSelector>();
|
||||
const entIds = new Map<string, { idx: number, members: Map<number, number> }>();
|
||||
const entColors = new Map<string, Color[]>();
|
||||
@@ -164,7 +173,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
for (let i = 0; i < entities._rowCount; i++) {
|
||||
const t = getEntityType(i);
|
||||
const color = entColors.get(t)![entIds.get(t)!.members.get(i)!];
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || 1;
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || (coarseGrained ? 2 : 1);
|
||||
|
||||
build = build
|
||||
.toRoot()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -35,6 +35,12 @@ function getSpacefillParams(color: Color, graphics: GraphicsMode) {
|
||||
clipPrimitive: true,
|
||||
approximate: gmp.approximate,
|
||||
alphaThickness: gmp.alphaThickness,
|
||||
interior: {
|
||||
color: Color.fromNormalizedRgb(0, 0, 0),
|
||||
colorStrength: 0.15,
|
||||
substance: { metalness: 0.0, roughness: 1.0, bumpiness: 0.0 },
|
||||
substanceStrength: 1,
|
||||
}
|
||||
},
|
||||
},
|
||||
colorTheme: {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,6 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
import { getAnimationParam } from '../../../mol-geo/geometry/animation';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
@@ -21,10 +22,10 @@ import { Hcl } from '../../../mol-util/color/spaces/hcl';
|
||||
import { StateObjectCell, StateObjectRef, StateSelection } from '../../../mol-state';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
|
||||
import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/representation/spacefill';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { saturate } from '../../../mol-math/interpolate';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
import { PCG } from '../../../mol-data/util/hash-functions';
|
||||
|
||||
function getHueRange(hue: number, variability: number) {
|
||||
let min = hue - variability;
|
||||
@@ -37,10 +38,11 @@ function getHueRange(hue: number, variability: number) {
|
||||
|
||||
function getGrayscaleColors(count: number, luminance: number, variability: number) {
|
||||
const out: Color[] = [];
|
||||
const pcg = new PCG();
|
||||
for (let i = 0; i < count; ++ i) {
|
||||
const l = saturate(luminance / 100);
|
||||
const v = saturate(variability / 180) * Math.random();
|
||||
const s = Math.random() > 0.5 ? 1 : -1;
|
||||
const v = saturate(variability / 180) * pcg.float();
|
||||
const s = pcg.float() > 0.5 ? 1 : -1;
|
||||
const d = Math.abs(l + s * v) % 1;
|
||||
out[i] = Color.fromNormalizedRgb(d, d, d);
|
||||
}
|
||||
@@ -172,6 +174,8 @@ export const LodParams = {
|
||||
approximate: Spheres.Params.approximate,
|
||||
};
|
||||
|
||||
export const AnimationParams = getAnimationParam().params;
|
||||
|
||||
export const SimpleClipParams = {
|
||||
type: PD.Select('none', PD.objectToOptions(Clip.Type, t => stringToWords(t))),
|
||||
invert: PD.Boolean(false),
|
||||
@@ -279,6 +283,7 @@ export const MesoscaleGroupParams = {
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
lod: PD.Group(LodParams),
|
||||
clip: PD.Group(SimpleClipParams),
|
||||
animation: PD.Group(AnimationParams),
|
||||
};
|
||||
export type MesoscaleGroupProps = PD.Values<typeof MesoscaleGroupParams>;
|
||||
|
||||
@@ -316,38 +321,7 @@ export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGr
|
||||
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
|
||||
|
||||
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
|
||||
switch (graphicsMode) {
|
||||
case 'performance':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
];
|
||||
case 'balanced':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
];
|
||||
case 'quality':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
];
|
||||
case 'ultra':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
];
|
||||
default:
|
||||
assertUnreachable(graphicsMode);
|
||||
}
|
||||
return Spheres.LodLevelsPresets[graphicsMode];
|
||||
}
|
||||
|
||||
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { EveryLoci, Loci } from '../../../mol-model/loci';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, AnimationParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import React, { useState } from 'react';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { StructureElement } from '../../../mol-model/structure/structure/element';
|
||||
@@ -828,6 +828,26 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
update.commit();
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
for (const r of this.allFilteredEntities) {
|
||||
update.to(r).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = values;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const g of this.allGroups) {
|
||||
update.to(g).update(old => {
|
||||
old.animation = values;
|
||||
});
|
||||
}
|
||||
|
||||
update.commit();
|
||||
};
|
||||
|
||||
update = (props: MesoscaleGroupProps) => {
|
||||
this.plugin.state.data.build().to(this.ref).update(props);
|
||||
};
|
||||
@@ -865,6 +885,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const rootValue = this.cell.params?.values.color;
|
||||
const clipValue = this.cell.params?.values.clip;
|
||||
const lodValue = this.cell.params?.values.lod;
|
||||
const animationValue = this.cell.params?.values.animation;
|
||||
const isRoot = this.cell.params?.values.root;
|
||||
|
||||
const groups = this.groups;
|
||||
@@ -904,6 +925,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterControls params={SimpleClipParams} values={clipValue} onChangeValues={this.updateClip} />
|
||||
<ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />
|
||||
<ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
{this.state.action === 'root' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
|
||||
@@ -1080,6 +1102,19 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
};
|
||||
}
|
||||
|
||||
get animationValue(): PD.Values<typeof AnimationParams> | undefined {
|
||||
const p = this.cell.transform.params?.type?.params?.animation;
|
||||
if (!p) return;
|
||||
return {
|
||||
wiggleMode: p.wiggleMode,
|
||||
wiggleSpeed: p.wiggleSpeed,
|
||||
wiggleAmplitude: p.wiggleAmplitude,
|
||||
wiggleFrequency: p.wiggleFrequency,
|
||||
tumbleSpeed: p.tumbleSpeed,
|
||||
tumbleAmplitude: p.tumbleAmplitude,
|
||||
};
|
||||
}
|
||||
|
||||
get patternValue(): { amplitude: number, frequency: number } | undefined {
|
||||
const p = this.cell.transform.params;
|
||||
if (p.type) return;
|
||||
@@ -1194,6 +1229,15 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
}
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const params = this.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
|
||||
if (!params.type) return;
|
||||
|
||||
this.plugin.build().to(this.ref).update(old => {
|
||||
old.type.params.animation = values;
|
||||
}).commit();
|
||||
};
|
||||
|
||||
updatePattern = (values: PD.Values) => {
|
||||
return this.plugin.build().to(this.ref).update(old => {
|
||||
if (!old.type) {
|
||||
@@ -1213,6 +1257,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
const opacityValue = this.opacityValue;
|
||||
const emissiveValue = this.emissiveValue;
|
||||
const lodValue = this.lodValue;
|
||||
const animationValue = this.animationValue;
|
||||
const patternValue = this.patternValue;
|
||||
|
||||
const l = getEntityLabel(this.plugin, this.cell);
|
||||
@@ -1251,6 +1296,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterMappingControl mapping={this.clipMapping} />
|
||||
{lodValue && <ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />}
|
||||
{animationValue && <ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />}
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
</>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { MesoscaleState } from '../data/state';
|
||||
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, MesoProceduralAnimationControls, ExplorerInfo } from './states';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
@@ -145,6 +145,7 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
<StructureMeasurementsControls initiallyCollapsed={true}/>
|
||||
</>
|
||||
<MesoQuickStylesControls />
|
||||
<MesoProceduralAnimationControls />
|
||||
<Spacer />
|
||||
<SectionHeader title='Entities' />
|
||||
<EntityControls />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,7 +8,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { AnimationSvg, GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
|
||||
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
|
||||
@@ -24,7 +24,7 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { getAllEntities, getAllGroups, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
@@ -46,8 +46,6 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
dimColor: Color(0xffffff),
|
||||
dimStrength: 1,
|
||||
markerPriority: 2,
|
||||
interiorColorFlag: false,
|
||||
interiorDarkening: 0.15,
|
||||
exposure: 1.1,
|
||||
xrayEdgeFalloff: 3,
|
||||
},
|
||||
@@ -781,3 +779,110 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export class MesoProceduralAnimationControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
header: 'Procedural Animation',
|
||||
brand: { accent: 'gray' as const, svg: AnimationSvg }
|
||||
};
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
return <>
|
||||
<MesoProceduralAnimation />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
class MesoProceduralAnimation extends PluginUIComponent {
|
||||
private isMembrane(cell: { transform: { tags?: string[] } }) {
|
||||
return cell.transform.tags?.some(t => t.includes('mem')) ?? false;
|
||||
}
|
||||
|
||||
async dynamics() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
const membrane = this.isMembrane(entity);
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
const membrane = this.isMembrane(group);
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow title='Enable wiggle for all entities and tumble for non-membrane entities' onClick={() => this.dynamics()} style={{ width: 'auto' }}>
|
||||
Dynamics
|
||||
</Button>
|
||||
<Button noOverflow title='Set wiggle and tumble amplitude to zero for all entities' onClick={() => this.clear()} style={{ width: 'auto' }}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
import { CSSProperties } from 'react';
|
||||
import { CSSProperties, useEffect, useState } from 'react';
|
||||
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
@@ -70,6 +70,28 @@ export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div>
|
||||
<div style={{ marginBottom: 16 }}><i>Loading times may vary depending on the story size, your internet connection, and device performance</i></div>
|
||||
<div>Fetching data<Dots /></div>
|
||||
<div>Generating animations<Dots /></div>
|
||||
<div>Preparing visuals<Dots /></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Dots() {
|
||||
const [dots, setDots] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDots(d => (d + 1) % 4);
|
||||
}, Math.random() * 500 + 300);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return <span>{'.'.repeat(dots)}</span>;
|
||||
}
|
||||
|
||||
export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnapshotMarkdownModel }) {
|
||||
const state = useBehavior(model.state);
|
||||
const isLoading = useBehavior(model.context.state.isLoading);
|
||||
@@ -79,7 +101,8 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={style} className={className}>
|
||||
<i>Loading...</i>
|
||||
<h3>The story will be ready momentarily</h3>
|
||||
<Loading />
|
||||
</div>;
|
||||
}
|
||||
|
||||
|
||||
@@ -94,8 +94,8 @@
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<span id="open-in-stories"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-molstar"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-stories" style="display: none;"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-molstar" style="display: none;"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a> <span class="sep">•</span></span>
|
||||
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep">•</span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -7,34 +7,20 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
|
||||
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
import { ZenodoImport } from '../../extensions/zenodo';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { StringLike } from '../../mol-io/common/string-like';
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { PluginComponent } from '../../mol-plugin-state/component';
|
||||
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
@@ -42,98 +28,39 @@ import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginBehaviors } from '../../mol-plugin/behavior';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
|
||||
import { MolScriptBuilder } from '../../mol-script/language/builder';
|
||||
import { Expression } from '../../mol-script/language/expression';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { StringLike } from '../../mol-io/common/string-like';
|
||||
import { ExtensionMap } from './extensions';
|
||||
import { DefaultViewerOptions, ViewerOptions } from './options';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
export { consoleStats, isDebugMode, isProductionMode, isTimingMode, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
export const ExtensionMap = {
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat),
|
||||
'model-export': PluginSpec.Behavior(ModelExport),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(ExtensionMap),
|
||||
disabledExtensions: [] as string[],
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
collapseLeftPanel: false,
|
||||
collapseRightPanel: false,
|
||||
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
|
||||
pixelScale: PluginConfig.General.PixelScale.defaultValue,
|
||||
pickScale: PluginConfig.General.PickScale.defaultValue,
|
||||
transparency: PluginConfig.General.Transparency.defaultValue,
|
||||
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
|
||||
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
|
||||
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
|
||||
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
|
||||
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
|
||||
config: [] as [PluginConfigItem, any][],
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
import { decodeColor } from '../../mol-util/color/utils';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ViewerAutoPreset } from './presets';
|
||||
import { CameraFocusOptions } from '../../mol-plugin-state/manager/camera';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { NoPrimaryFocusLociBindings } from '../../mol-plugin/behavior/dynamic/camera';
|
||||
|
||||
export class Viewer {
|
||||
constructor(public plugin: PluginUIContext) {
|
||||
private _events = new PluginComponent();
|
||||
public readonly plugin: PluginUIContext;
|
||||
|
||||
constructor(plugin: PluginUIContext) {
|
||||
this.plugin = plugin;
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
|
||||
@@ -148,11 +75,31 @@ export class Viewer {
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const disabledExtension = new Set(o.disabledExtensions ?? []);
|
||||
let baseBehaviors = defaultSpec.behaviors;
|
||||
|
||||
if (o.viewportFocusBehavior === 'disabled') {
|
||||
baseBehaviors = baseBehaviors.filter(b =>
|
||||
b.transformer !== PluginBehaviors.Camera.FocusLoci
|
||||
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
|
||||
);
|
||||
} else if (o.viewportFocusBehavior === 'secondary-zoom') {
|
||||
baseBehaviors = baseBehaviors.filter(b =>
|
||||
b.transformer !== PluginBehaviors.Camera.FocusLoci
|
||||
&& b.transformer !== PluginBehaviors.Representation.FocusLoci
|
||||
);
|
||||
|
||||
baseBehaviors.push(PluginSpec.Behavior(PluginBehaviors.Camera.FocusLoci, {
|
||||
bindings: NoPrimaryFocusLociBindings
|
||||
}));
|
||||
}
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
canvas3d: {
|
||||
...defaultSpec.canvas3d,
|
||||
},
|
||||
actions: defaultSpec.actions,
|
||||
behaviors: [
|
||||
...defaultSpec.behaviors,
|
||||
...baseBehaviors,
|
||||
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
@@ -228,10 +175,23 @@ export class Viewer {
|
||||
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
|
||||
}
|
||||
});
|
||||
|
||||
plugin.canvas3d?.setProps({ illumination: { enabled: o.illumination } });
|
||||
if (o.viewportBackgroundColor) {
|
||||
const backgroundColor = decodeColor(o.viewportBackgroundColor);
|
||||
if (typeof backgroundColor === 'number') {
|
||||
plugin.canvas3d?.setProps({ renderer: { backgroundColor } });
|
||||
}
|
||||
}
|
||||
return new Viewer(plugin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows subscribing to rxjs observables in the context of the viewer.
|
||||
* All subscriptions will be disposed of when the viewer is destroyed.
|
||||
*/
|
||||
subscribe = this._events.subscribe.bind(this._events);
|
||||
|
||||
setRemoteSnapshot(id: string) {
|
||||
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
|
||||
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
|
||||
@@ -523,7 +483,7 @@ export class Viewer {
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
if (format === 'mvsj') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
|
||||
const mvsData = MVSData.fromMVSJ(StringLike.toString(data));
|
||||
@@ -531,7 +491,7 @@ export class Viewer {
|
||||
} else if (format === 'mvsx') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
|
||||
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data);
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
|
||||
}));
|
||||
} else {
|
||||
@@ -542,7 +502,7 @@ export class Viewer {
|
||||
/** Load MolViewSpec from `data`.
|
||||
* If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string.
|
||||
* If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */
|
||||
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
return loadMVSData(this.plugin, data, format, options);
|
||||
}
|
||||
|
||||
@@ -567,7 +527,56 @@ export class Viewer {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggers structure element selection or highlighting based on the provided
|
||||
* MolScript expression or StructureElement schema. Focus action will only apply to the
|
||||
* first structure that matches the criteria.
|
||||
*
|
||||
* If neither `expression` nor `elements` are provided, all selections/highlights
|
||||
* will be cleared based on the specified `action`.
|
||||
*/
|
||||
structureInteractivity({ expression, elements, action, applyGranularity = false, filterStructure, focusOptions }: {
|
||||
expression?: (queryBuilder: typeof MolScriptBuilder) => Expression,
|
||||
elements?: StructureElement.Schema,
|
||||
action: 'highlight' | 'select' | 'focus',
|
||||
applyGranularity?: boolean,
|
||||
filterStructure?: (structure: Structure) => boolean,
|
||||
focusOptions?: Partial<CameraFocusOptions>
|
||||
}) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!expression && !elements) {
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.clearHighlights();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const structures = this.plugin.state.data.selectQ(Q => Q.rootsOfType(PluginStateObject.Molecule.Structure));
|
||||
for (const s of structures) {
|
||||
if (!s.obj?.data) continue;
|
||||
|
||||
if (filterStructure && !filterStructure(s.obj.data)) continue;
|
||||
|
||||
const loci = expression
|
||||
? StructureElement.Loci.fromExpression(s.obj.data, expression)
|
||||
: StructureElement.Loci.fromSchema(s.obj.data, elements!);
|
||||
|
||||
if (action === 'select') {
|
||||
plugin.managers.interactivity.lociSelects.select({ loci }, applyGranularity);
|
||||
} else if (action === 'highlight') {
|
||||
plugin.managers.interactivity.lociHighlights.highlight({ loci }, applyGranularity);
|
||||
} else if (action === 'focus' && !StructureElement.Loci.isEmpty(loci)) {
|
||||
plugin.managers.camera.focusLoci(loci, focusOptions);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this._events.dispose();
|
||||
this.plugin.dispose();
|
||||
}
|
||||
}
|
||||
@@ -594,44 +603,4 @@ export interface LoadTrajectoryParams {
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInCoordinatesFormat },
|
||||
coordinatesLabel?: string,
|
||||
preset?: keyof PresetTrajectoryHierarchy
|
||||
}
|
||||
|
||||
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-viewer-auto',
|
||||
display: {
|
||||
name: 'Automatic (w/ Annotation)', group: 'Annotation',
|
||||
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return (
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
|
||||
);
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
|
||||
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS, loadMVSData },
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
74
src/apps/viewer/extensions.ts
Normal file
74
src/apps/viewer/extensions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2026 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>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DebugHelpers } from '../../extensions/debug-helpers';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment, MAQualityAssessmentConfig } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { loadMVS } from '../../extensions/mvs';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVSData } from '../../extensions/mvs/components/formats';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { SbNcbrPartialCharges, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
import { ZenodoImport } from '../../extensions/zenodo';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import * as MVSUtil from '../../extensions/mvs/util';
|
||||
import { KinemageExtension } from '../../extensions/kinemage/behavior';
|
||||
|
||||
export const ExtensionMap = {
|
||||
// Mol* built-in extensions
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'debug-helpers': PluginSpec.Behavior(DebugHelpers),
|
||||
'model-export': PluginSpec.Behavior(ModelExport),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'kinemage': PluginSpec.Behavior(KinemageExtension),
|
||||
|
||||
// 3rd party extensions
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
|
||||
'assembly-symmetry': PluginSpec.Behavior(AssemblySymmetry),
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat), // TODO: consider removing this for Mol* 6.0
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
|
||||
};
|
||||
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: {
|
||||
MVSData,
|
||||
createBuilder: MVSData.createBuilder,
|
||||
loadMVS,
|
||||
loadMVSData,
|
||||
util: {
|
||||
...MVSUtil
|
||||
}
|
||||
},
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,12 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 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 './mvs.html';
|
||||
import './embedded.html';
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
export * from './lib';
|
||||
export * from './extensions';
|
||||
export * from './app';
|
||||
export * from './presets';
|
||||
|
||||
58
src/apps/viewer/lib.ts
Normal file
58
src/apps/viewer/lib.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as Structure from '../../mol-model/structure';
|
||||
import { DataLoci, EveryLoci, Loci } from '../../mol-model/loci';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { Shape, ShapeGroup } from '../../mol-model/shape';
|
||||
import * as LinearAlgebra3D from '../../mol-math/linear-algebra/3d';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { StateActions } from '../../mol-plugin-state/actions';
|
||||
import { PluginExtensions } from './extensions';
|
||||
|
||||
export const lib = {
|
||||
structure: {
|
||||
...Structure,
|
||||
},
|
||||
volume: {
|
||||
Volume,
|
||||
},
|
||||
shape: {
|
||||
Shape,
|
||||
ShapeGroup,
|
||||
},
|
||||
loci: {
|
||||
Loci,
|
||||
DataLoci,
|
||||
EveryLoci,
|
||||
},
|
||||
math: {
|
||||
LinearAlgebra: {
|
||||
...LinearAlgebra3D,
|
||||
}
|
||||
},
|
||||
plugin: {
|
||||
PluginContext,
|
||||
PluginConfig,
|
||||
PluginBehavior,
|
||||
PluginSpec,
|
||||
PluginStateObject,
|
||||
PluginStateTransform,
|
||||
StateTransforms,
|
||||
StateActions,
|
||||
DefaultPluginSpec,
|
||||
DefaultPluginUISpec,
|
||||
},
|
||||
extensions: {
|
||||
...PluginExtensions
|
||||
}
|
||||
};
|
||||
179
src/apps/viewer/mvs.html
Normal file
179
src/apps/viewer/mvs.html
Normal file
@@ -0,0 +1,179 @@
|
||||
<!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">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer MolViewSpec Example</title>
|
||||
<style>
|
||||
body {
|
||||
background: #111318;
|
||||
}
|
||||
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-family: sans-serif;
|
||||
gap: 8px;
|
||||
left: 10px;
|
||||
top: 10px;
|
||||
z-index: 10;
|
||||
background-color: #111318;
|
||||
padding: 10px;
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="theme/dark.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="controls">
|
||||
<button onmouseenter="interactivy('highlight')" onmouseleave="interactivy('clear-highlight')" onclick="interactivy('select')">Select Residues 45-50</button>
|
||||
<button onmouseenter="interactivy('highlight')" onmouseleave="interactivy('clear-highlight')" onclick="interactivy('focus')">Focus</button>
|
||||
<button onclick="interactivy('clear-select')">Clear Selection</button>
|
||||
<div id="selection-info"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
function interactivy(action) {
|
||||
if (action === 'clear-highlight') {
|
||||
viewer.structureInteractivity({ action: 'highlight' });
|
||||
} else if (action === 'clear-select') {
|
||||
viewer.structureInteractivity({ action: 'select' });
|
||||
} else if (action === 'highlight' || action === 'select' || action === 'focus') {
|
||||
viewer.structureInteractivity({
|
||||
elements: { beg_auth_seq_id: 45, end_auth_seq_id: 50 },
|
||||
action,
|
||||
focusOptions: { extraRadius: 3 }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
viewer.structureInteractivity({ action: 'select' });
|
||||
}
|
||||
|
||||
molstar.Viewer.create('app', {
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: false,
|
||||
layoutShowLeftPanel: true,
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowControls: false,
|
||||
viewportShowAnimation: false,
|
||||
viewportFocusBehavior: 'secondary-zoom',
|
||||
viewportBackgroundColor: '#111318',
|
||||
|
||||
pdbProvider: 'rcsb',
|
||||
emdbProvider: 'rcsb',
|
||||
}).then(viewer => {
|
||||
// Make the viewer accessible globally for the demo buttons
|
||||
window.viewer = viewer;
|
||||
|
||||
// Build MVS state
|
||||
const builder = molstar.lib.extensions.mvs.createBuilder();
|
||||
const structure = builder
|
||||
.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif' })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure({});
|
||||
structure
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'cartoon' })
|
||||
.color({ color: 'green' });
|
||||
structure
|
||||
.component({ selector: 'ligand' })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({ color: '#cc3399' });
|
||||
|
||||
// Extra data can be passed to the MVS snapshot via custom state
|
||||
// and later accessed it using getCurrentMVSSnapshot() (see hover handler below)
|
||||
// Each node can have custom data as well, but generally could be harder to access
|
||||
// This example is a little contrived to demonstrate the concept
|
||||
builder.extendRootCustomState({
|
||||
extraResidueAnnotations: {
|
||||
'REA': 'Ligand'
|
||||
}
|
||||
})
|
||||
|
||||
builder.canvas({
|
||||
background_color: "#111318",
|
||||
})
|
||||
|
||||
structure.primitives()
|
||||
.sphere({
|
||||
center: { label_comp_id: 'REA' },
|
||||
radius: 3,
|
||||
custom: { action: 'Action 1' },
|
||||
})
|
||||
.label({
|
||||
text: '1',
|
||||
position: { label_comp_id: 'REA' },
|
||||
label_size: 2.5,
|
||||
label_color: 'blue',
|
||||
});
|
||||
|
||||
structure.primitives()
|
||||
.sphere({
|
||||
center: { label_seq_id: 2 },
|
||||
radius: 3,
|
||||
custom: { action: 'Action 2' },
|
||||
})
|
||||
.label({
|
||||
text: '2',
|
||||
position: { label_seq_id: 2 },
|
||||
label_size: 2.5,
|
||||
label_color: 'blue',
|
||||
});
|
||||
|
||||
const mvsData = builder.getState();
|
||||
|
||||
viewer.loadMvsData(mvsData, 'mvsj');
|
||||
|
||||
// Show current residue interaction
|
||||
viewer.subscribe(viewer.plugin.behaviors.interaction.hover, e => {
|
||||
const infoElement = document.getElementById('selection-info');
|
||||
if (!infoElement) return;
|
||||
|
||||
if (molstar.lib.structure.StructureElement.Loci.is(e.current.loci)) {
|
||||
molstar.lib.structure.StructureElement.Loci.forEachLocation(e.current.loci, location => {
|
||||
const props = molstar.lib.structure.StructureProperties;
|
||||
let label = `Hovered Residue: ${props.chain.label_asym_id(location)} ${props.residue.label_seq_id(location)}`;
|
||||
|
||||
const compId = props.residue.label_comp_id(location);
|
||||
const snapshot = molstar.lib.extensions.mvs.util.getCurrentMVSSnapshot(viewer.plugin);
|
||||
if (snapshot && snapshot.root.custom && snapshot.root.custom.extraResidueAnnotations) {
|
||||
const extra = snapshot.root.custom.extraResidueAnnotations[compId];
|
||||
if (extra) label += ` (${extra})`;
|
||||
}
|
||||
|
||||
infoElement.innerText = label;
|
||||
});
|
||||
} else {
|
||||
infoElement.innerText = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Show clicked primitive action
|
||||
viewer.subscribe(viewer.plugin.behaviors.interaction.click, e => {
|
||||
const nodes = molstar.lib.extensions.mvs.util.tryGetPrimitivesFromLoci(e.current.loci);
|
||||
if (nodes?.length) {
|
||||
alert('Clicked on: ' + (nodes[0].custom?.action || 'unknown'));
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
72
src/apps/viewer/options.ts
Normal file
72
src/apps/viewer/options.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 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 { AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { G3dProvider } from '../../extensions/g3d/format';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { ExtensionMap } from './extensions';
|
||||
|
||||
const CustomFormats: [string, DataFormatProvider][] = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
export const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(ExtensionMap),
|
||||
disabledExtensions: [] as string[],
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
layoutControlsDisplay: 'reactive' as PluginLayoutControlsDisplay,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
collapseLeftPanel: false,
|
||||
collapseRightPanel: false,
|
||||
disableAntialiasing: PluginConfig.General.DisableAntialiasing.defaultValue,
|
||||
pixelScale: PluginConfig.General.PixelScale.defaultValue,
|
||||
pickScale: PluginConfig.General.PickScale.defaultValue,
|
||||
transparency: PluginConfig.General.Transparency.defaultValue,
|
||||
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
|
||||
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
|
||||
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
|
||||
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
viewportShowTrajectoryControls: PluginConfig.Viewport.ShowTrajectoryControls.defaultValue,
|
||||
// default: zoom & show structure interaction
|
||||
// secondary-zoom: zoom only, doesn't use primary mouse button
|
||||
// disabled: no automatic zoom or interaction on focus
|
||||
viewportFocusBehavior: 'default' as 'default' | 'secondary-zoom' | 'disabled',
|
||||
viewportBackgroundColor: undefined as string | undefined,
|
||||
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
|
||||
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
|
||||
config: [] as [PluginConfigItem, any][],
|
||||
};
|
||||
export type ViewerOptions = typeof DefaultViewerOptions;
|
||||
42
src/apps/viewer/presets.ts
Normal file
42
src/apps/viewer/presets.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../mol-state';
|
||||
|
||||
export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-viewer-auto',
|
||||
display: {
|
||||
name: 'Automatic (w/ Annotation)', group: 'Annotation',
|
||||
description: 'Show standard automatic representation but colored by quality assessment (if available in the model).'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return (
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')) ||
|
||||
!!a.data.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))
|
||||
);
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT'))) {
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
|
||||
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
7
src/apps/viewer/theme/blue.ts
Normal file
7
src/apps/viewer/theme/blue.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import '../../../mol-plugin-ui/skin/blue.scss';
|
||||
7
src/apps/viewer/theme/dark.ts
Normal file
7
src/apps/viewer/theme/dark.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import '../../../mol-plugin-ui/skin/dark.scss';
|
||||
7
src/apps/viewer/theme/light.ts
Normal file
7
src/apps/viewer/theme/light.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import '../../../mol-plugin-ui/skin/light.scss';
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.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);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -32,7 +31,7 @@ function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
@@ -41,7 +40,7 @@ function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.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);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -44,7 +43,7 @@ function writeSaccharideNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -250,14 +249,14 @@ async function run(out: string, binary = false, options = DefaultDataOptions, cc
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeFile(out, ccbCif);
|
||||
writeFileAsync(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);
|
||||
writeFileAsync(ccaOut, ccaCif);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
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);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database } from '../../mol-data/db';
|
||||
@@ -27,9 +25,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
await writeFileAsync(path, zlib.gunzipSync(await data.arrayBuffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
await writeFileAsync(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
@@ -41,7 +39,7 @@ export async function ensureDataAvailable(options: DataOptions) {
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
const parsed = await parseCif(await readFileAsync(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { CIF, CifCategory, getCifFieldType, CifField, CifFile } from '../../mol-io/reader/cif';
|
||||
@@ -22,7 +23,7 @@ function showProgress(p: Progress) {
|
||||
process.stdout.write(`\r${Progress.format(p)}`);
|
||||
}
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const unzipAsync = util.promisify<zlib.InputType, Buffer>(zlib.unzip);
|
||||
|
||||
async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderResult<CifFile>> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import { convert } from './converter';
|
||||
|
||||
require('util.promisify').shim();
|
||||
|
||||
async function process(srcPath: string, outPath: string, configPath?: string, filterPath?: string) {
|
||||
const config = configPath ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : void 0;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { parseCsv } from '../../mol-io/reader/csv/parser';
|
||||
import { CifFrame, CifBlock } from '../../mol-io/reader/cif';
|
||||
@@ -166,9 +166,9 @@ const MA_DIC_URL = 'https://raw.githubusercontent.com/ihmwg/ModelCIF/master/dist
|
||||
const CIF_CORE_DIC_PATH = `${DIC_DIR}/cif_core.dic`;
|
||||
const CIF_CORE_DIC_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic';
|
||||
const CIF_CORE_ENUM_PATH = `${DIC_DIR}/templ_enum.cif`;
|
||||
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_enum.cif';
|
||||
const CIF_CORE_ENUM_URL = 'https://raw.githubusercontent.com/COMCIFS/Enumeration_Templates/refs/heads/main/templ_enum.cif';
|
||||
const CIF_CORE_ATTR_PATH = `${DIC_DIR}/templ_attr.cif`;
|
||||
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/cif_core/master/templ_attr.cif';
|
||||
const CIF_CORE_ATTR_URL = 'https://raw.githubusercontent.com/COMCIFS/Attribute_Templates/refs/heads/main/templ_attr.cif';
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
|
||||
@@ -93,6 +93,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'Implied':
|
||||
case 'Word':
|
||||
case 'Uri':
|
||||
case 'Iri':
|
||||
return wrapContainer('str', ',', description, container);
|
||||
case 'Real':
|
||||
return wrapContainer('float', ',', description, container);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -65,7 +65,9 @@ function getTypeDef(c: Column): string {
|
||||
case 'float': return 'float';
|
||||
case 'coord': return 'coord';
|
||||
case 'enum':
|
||||
return `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
return c.subType === 'int'
|
||||
? `Aliased<${c.values.join(' | ')}>(${c.subType})`
|
||||
: `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
case 'matrix':
|
||||
return `Matrix(${c.rows}, ${c.columns})`;
|
||||
case 'vector':
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
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/');
|
||||
@@ -33,6 +33,14 @@ async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PA
|
||||
|
||||
const extraLipids = ['DMPC'];
|
||||
const v2lipids = ['DAPC', 'DBPC', 'DFPC', 'DGPC', 'DIPC', 'DLPC', 'DNPC', 'DOPC', 'DPPC', 'DRPC', 'DTPC', 'DVPC', 'DXPC', 'DYPC', 'LPPC', 'PAPC', 'PEPC', 'PGPC', 'PIPC', 'POPC', 'PRPC', 'PUPC', 'DAPE', 'DBPE', 'DFPE', 'DGPE', 'DIPE', 'DLPE', 'DNPE', 'DOPE', 'DPPE', 'DRPE', 'DTPE', 'DUPE', 'DVPE', 'DXPE', 'DYPE', 'LPPE', 'PAPE', 'PGPE', 'PIPE', 'POPE', 'PQPE', 'PRPE', 'PUPE', 'DAPS', 'DBPS', 'DFPS', 'DGPS', 'DIPS', 'DLPS', 'DNPS', 'DOPS', 'DPPS', 'DRPS', 'DTPS', 'DUPS', 'DVPS', 'DXPS', 'DYPS', 'LPPS', 'PAPS', 'PGPS', 'PIPS', 'POPS', 'PQPS', 'PRPS', 'PUPS', 'DAPG', 'DBPG', 'DFPG', 'DGPG', 'DIPG', 'DLPG', 'DNPG', 'DOPG', 'DPPG', 'DRPG', 'DTPG', 'DVPG', 'DXPG', 'DYPG', 'LPPG', 'PAPG', 'PGPG', 'PIPG', 'POPG', 'PRPG', 'DAPA', 'DBPA', 'DFPA', 'DGPA', 'DIPA', 'DLPA', 'DNPA', 'DOPA', 'DPPA', 'DRPA', 'DTPA', 'DVPA', 'DXPA', 'DYPA', 'LPPA', 'PAPA', 'PGPA', 'PIPA', 'POPA', 'PRPA', 'PUPA', 'DPP', 'DPPI', 'PAPI', 'PIPI', 'POP', 'POPI', 'PUPI', 'PVP', 'PVPI', 'PADG', 'PIDG', 'PODG', 'PUDG', 'PVDG', 'APC', 'CPC', 'IPC', 'LPC', 'OPC', 'PPC', 'TPC', 'UPC', 'VPC', 'BNSM', 'DBSM', 'DPSM', 'DXSM', 'PGSM', 'PNSM', 'POSM', 'PVSM', 'XNSM', 'DPCE', 'DXCE', 'PNCE', 'XNCE'];
|
||||
const amberLipids = [
|
||||
// acyl chains
|
||||
'PA', 'ST', 'OL', 'LEO', 'LEN', 'AR', 'DHA',
|
||||
// head groups
|
||||
'PC', 'PE', 'PS', 'PH-', 'P2-', 'PGR', 'PGS', 'PI',
|
||||
// other
|
||||
'CHL'
|
||||
];
|
||||
|
||||
async function run(out: string) {
|
||||
await ensureLipidsAvailable();
|
||||
@@ -55,13 +63,17 @@ async function run(out: string) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
for (const v of amberLipids) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
const lipidNames = JSON.stringify(lipids.array);
|
||||
|
||||
if (out) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
|
||||
* Code-generated lipid params file. Names from Martini FF and Amber.
|
||||
*
|
||||
* @author molstar/lipid-params cli
|
||||
*/
|
||||
|
||||
60
src/cli/mvs/mvs-mvsj-to-mvsx.ts
Normal file
60
src/cli/mvs/mvs-mvsj-to-mvsx.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for converting MolViewSpec MVSJ into MSVX files
|
||||
* Build: npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-mvsj-to-mvsx -i examples/mvs/1cbs.mvsj -o tmp/1cbs.mvsx
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
/** Command line argument values for `main` */
|
||||
interface Args {
|
||||
input: string[],
|
||||
output: string[] | undefined,
|
||||
base_uri: string | undefined,
|
||||
skip_external: boolean,
|
||||
}
|
||||
|
||||
/** Return parsed command line arguments for `main` */
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Command-line application for converting MolViewSpec MVSJ into MSVX files' });
|
||||
parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj format.' });
|
||||
parser.add_argument('-o', '--output', { required: false, nargs: '+', help: 'File path(s) for output files in .mvsx format (one output path for each input file). If ommitted, filenames will be created automatically by replacing file extension.' });
|
||||
parser.add_argument('--base-uri', { help: 'Base URI/path used to resolve relative URIs in the input file (default: path of the input file itself). Use `--base-uri .` for using the current working directory as base URI.' });
|
||||
parser.add_argument('--skip-external', { action: 'store_true', help: 'Do not include external resources (i.e. absolute URIs) in the MVSX.' });
|
||||
const args: Args = parser.parse_args();
|
||||
if (args.output && args.output.length !== args.input.length) {
|
||||
parser.error(`argument: --output: must specify the same number of input and output file paths (specified ${args.input.length} input path${args.input.length !== 1 ? 's' : ''} but ${args.output.length} output path${args.output.length !== 1 ? 's' : ''})`);
|
||||
}
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
/** Main workflow for converting MVSJ to MVSX files. */
|
||||
async function main(args: Args): Promise<void> {
|
||||
const cache = {};
|
||||
for (let i = 0; i < args.input.length; i++) {
|
||||
const input = args.input[i];
|
||||
const output = args.output?.[i] ?? input.replace(/(\.mvsj)?$/i, '.mvsx');
|
||||
console.log(`Processing ${input} -> ${output}`);
|
||||
const mvsj = fs.readFileSync(input, { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(mvsj);
|
||||
const mvsx = await MVSData.toMVSX(mvsData, {
|
||||
baseUri: args.base_uri ?? input,
|
||||
skipExternal: args.skip_external,
|
||||
cache,
|
||||
});
|
||||
fs.writeFileSync(output, mvsx);
|
||||
}
|
||||
}
|
||||
|
||||
main(parseArguments());
|
||||
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { Progress } from '../../mol-task';
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
|
||||
async function readFile(path: string) {
|
||||
if (path.match(/\.bcif$/)) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 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>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { downloadCif } from './helpers';
|
||||
@@ -19,8 +20,7 @@ import { createVolumeIsosurfaceMesh } from '../../mol-repr/volume/isosurface';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { volumeFromDensityServerData, DscifFormat } from '../../mol-model-formats/volume/density-server';
|
||||
|
||||
require('util.promisify').shim();
|
||||
const writeFileAsync = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
async function getVolume(url: string): Promise<Volume> {
|
||||
const cif = await downloadCif(url, true);
|
||||
@@ -38,7 +38,7 @@ function print(volume: Volume) {
|
||||
}
|
||||
|
||||
async function doMesh(volume: Volume, filename: string) {
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto', floodfill: 'off' })).run();
|
||||
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
|
||||
|
||||
// Export the mesh in OBJ format.
|
||||
|
||||
@@ -21,8 +21,10 @@ import { StripedResidues } from './coloring';
|
||||
import { CustomToastMessage } from './controls';
|
||||
import { CustomColorThemeProvider } from './custom-theme';
|
||||
import './index.html';
|
||||
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
|
||||
import './tm-align.html';
|
||||
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData, tmAlignStructures, loadStructuresNoAlignment, sequenceAlignStructures } from './superposition';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
@@ -94,7 +96,7 @@ class BasicWrapper {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -190,6 +192,45 @@ class BasicWrapper {
|
||||
PluginCommands.Toast.Hide(this.plugin, { key: 'toast-2' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Run TM-align on two structures
|
||||
* @param pdbId1 - PDB ID of first structure (reference)
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure (mobile)
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
tmAlign(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
|
||||
return tmAlignStructures(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load two structures without alignment
|
||||
* @param pdbId1 - PDB ID of first structure
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
loadStructures(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
|
||||
return loadStructuresNoAlignment(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Align two structures using sequence alignment
|
||||
* @param pdbId1 - PDB ID of first structure (reference)
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure (mobile)
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
sequenceAlign(pdbId1: string, chain1: string, pdbId2: string, chain2: string, color1?: number, color2?: number) {
|
||||
return sequenceAlignStructures(this.plugin, pdbId1, chain1, pdbId2, chain2, color1, color2);
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).BasicMolStarWrapper = new BasicWrapper();
|
||||
@@ -5,8 +5,9 @@
|
||||
*/
|
||||
|
||||
import { Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { QueryContext, StructureSelection } from '../../mol-model/structure';
|
||||
import { superpose } from '../../mol-model/structure/structure/util/superposition';
|
||||
import { QueryContext, StructureSelection, StructureElement } from '../../mol-model/structure';
|
||||
import { superpose, alignAndSuperpose } from '../../mol-model/structure/structure/util/superposition';
|
||||
import { tmAlign } from '../../mol-model/structure/structure/util/tm-align';
|
||||
import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
@@ -116,4 +117,217 @@ function transform(plugin: PluginContext, s: StateObjectRef<PSO.Molecule.Structu
|
||||
const b = plugin.state.data.build().to(s)
|
||||
.insert(StateTransforms.Model.TransformStructureConformation, { transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
|
||||
return plugin.runTask(plugin.state.data.updateTree(b));
|
||||
}
|
||||
|
||||
export interface TMAlignResult {
|
||||
tmScoreA: number;
|
||||
tmScoreB: number;
|
||||
rmsd: number;
|
||||
alignedLength: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* TM-align superposition: aligns two structures using TM-align algorithm
|
||||
* @param plugin - Mol* plugin context
|
||||
* @param pdbId1 - PDB ID of first structure (reference)
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure (mobile)
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
export async function tmAlignStructures(
|
||||
plugin: PluginContext,
|
||||
pdbId1: string,
|
||||
chain1: string,
|
||||
pdbId2: string,
|
||||
chain2: string,
|
||||
color1: number = 0x3498db,
|
||||
color2: number = 0xe74c3c
|
||||
): Promise<TMAlignResult | undefined> {
|
||||
await plugin.clear();
|
||||
|
||||
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
|
||||
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
|
||||
const label1 = `${pdbId1} Chain ${chain1}`;
|
||||
const label2 = `${pdbId2} Chain ${chain2}`;
|
||||
|
||||
// Load structures
|
||||
const struct1 = await loadStructure(plugin, url1, 'pdb');
|
||||
const struct2 = await loadStructure(plugin, url2, 'pdb');
|
||||
|
||||
// Build query for C-alpha atoms from specified chains
|
||||
const caQuery1 = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain1]),
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
const caQuery2 = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain2]),
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
|
||||
const structure1Data = struct1.structure.cell?.obj?.data;
|
||||
const structure2Data = struct2.structure.cell?.obj?.data;
|
||||
|
||||
if (!structure1Data || !structure2Data) {
|
||||
console.error('Failed to load structures');
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery1(new QueryContext(structure1Data)));
|
||||
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery2(new QueryContext(structure2Data)));
|
||||
|
||||
const loci1 = StructureElement.Loci.is(sel1) ? sel1 : StructureElement.Loci.none(structure1Data);
|
||||
const loci2 = StructureElement.Loci.is(sel2) ? sel2 : StructureElement.Loci.none(structure2Data);
|
||||
|
||||
if (StructureElement.Loci.size(loci1) === 0 || StructureElement.Loci.size(loci2) === 0) {
|
||||
console.error('Empty selection - cannot run TM-align');
|
||||
// Still show the structures without alignment
|
||||
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
|
||||
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Run TM-align
|
||||
const result = tmAlign(loci1, loci2);
|
||||
|
||||
console.log('TM-score (structure 1):', result.tmScoreA.toFixed(5));
|
||||
console.log('TM-score (structure 2):', result.tmScoreB.toFixed(5));
|
||||
console.log('RMSD:', result.rmsd.toFixed(2), 'A');
|
||||
console.log('Aligned residues:', result.alignedLength);
|
||||
|
||||
// Apply the transformation to superimpose structure 2 onto structure 1
|
||||
await transform(plugin, struct2.structure, result.bTransform);
|
||||
|
||||
// Add cartoon representations
|
||||
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
|
||||
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
|
||||
|
||||
return {
|
||||
tmScoreA: result.tmScoreA,
|
||||
tmScoreB: result.tmScoreB,
|
||||
rmsd: result.rmsd,
|
||||
alignedLength: result.alignedLength
|
||||
};
|
||||
}
|
||||
|
||||
async function addChainRepresentation(
|
||||
plugin: PluginContext,
|
||||
structure: StateObjectRef<PSO.Molecule.Structure>,
|
||||
chain: string,
|
||||
label: string,
|
||||
color: number
|
||||
) {
|
||||
const component = await plugin.builders.structure.tryCreateComponentFromExpression(
|
||||
structure,
|
||||
chainSelection(chain),
|
||||
label
|
||||
);
|
||||
if (component) {
|
||||
await plugin.builders.structure.representation.addRepresentation(component, {
|
||||
type: 'cartoon',
|
||||
color: 'uniform',
|
||||
colorParams: { value: color }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and display two structures without any alignment
|
||||
* @param plugin - Mol* plugin context
|
||||
* @param pdbId1 - PDB ID of first structure
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
export async function loadStructuresNoAlignment(
|
||||
plugin: PluginContext,
|
||||
pdbId1: string,
|
||||
chain1: string,
|
||||
pdbId2: string,
|
||||
chain2: string,
|
||||
color1: number = 0x3498db,
|
||||
color2: number = 0xe74c3c
|
||||
): Promise<void> {
|
||||
await plugin.clear();
|
||||
|
||||
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
|
||||
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
|
||||
const label1 = `${pdbId1} Chain ${chain1}`;
|
||||
const label2 = `${pdbId2} Chain ${chain2}`;
|
||||
|
||||
const struct1 = await loadStructure(plugin, url1, 'pdb');
|
||||
const struct2 = await loadStructure(plugin, url2, 'pdb');
|
||||
|
||||
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
|
||||
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
|
||||
|
||||
console.log('Loaded structures - NO ALIGNMENT');
|
||||
}
|
||||
|
||||
/**
|
||||
* Sequence-based superposition: aligns two structures using sequence alignment + RMSD minimization
|
||||
* @param plugin - Mol* plugin context
|
||||
* @param pdbId1 - PDB ID of first structure (reference)
|
||||
* @param chain1 - Chain ID of first structure
|
||||
* @param pdbId2 - PDB ID of second structure (mobile)
|
||||
* @param chain2 - Chain ID of second structure
|
||||
* @param color1 - Optional color for first structure (hex, default blue)
|
||||
* @param color2 - Optional color for second structure (hex, default red)
|
||||
*/
|
||||
export async function sequenceAlignStructures(
|
||||
plugin: PluginContext,
|
||||
pdbId1: string,
|
||||
chain1: string,
|
||||
pdbId2: string,
|
||||
chain2: string,
|
||||
color1: number = 0x3498db,
|
||||
color2: number = 0xe74c3c
|
||||
): Promise<{ rmsd: number }> {
|
||||
await plugin.clear();
|
||||
|
||||
const url1 = `https://files.rcsb.org/download/${pdbId1}.pdb`;
|
||||
const url2 = `https://files.rcsb.org/download/${pdbId2}.pdb`;
|
||||
const label1 = `${pdbId1} Chain ${chain1}`;
|
||||
const label2 = `${pdbId2} Chain ${chain2}`;
|
||||
|
||||
const struct1 = await loadStructure(plugin, url1, 'pdb');
|
||||
const struct2 = await loadStructure(plugin, url2, 'pdb');
|
||||
|
||||
// Build queries for C-alpha atoms from specified chains
|
||||
const caQuery1 = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain1]),
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
const caQuery2 = compile<StructureSelection>(MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.auth_asym_id(), chain2]),
|
||||
'atom-test': MS.core.rel.eq([MS.struct.atomProperty.macromolecular.label_atom_id(), 'CA'])
|
||||
}));
|
||||
|
||||
const structure1Data = struct1.structure.cell?.obj?.data;
|
||||
const structure2Data = struct2.structure.cell?.obj?.data;
|
||||
|
||||
if (!structure1Data || !structure2Data) {
|
||||
console.error('Failed to load structures');
|
||||
return { rmsd: 0 };
|
||||
}
|
||||
|
||||
const sel1 = StructureSelection.toLociWithCurrentUnits(caQuery1(new QueryContext(structure1Data)));
|
||||
const sel2 = StructureSelection.toLociWithCurrentUnits(caQuery2(new QueryContext(structure2Data)));
|
||||
|
||||
// Run sequence alignment + superposition
|
||||
const transforms = alignAndSuperpose([sel1, sel2]);
|
||||
|
||||
// Apply the transformation to superimpose structure 2 onto structure 1
|
||||
await transform(plugin, struct2.structure, transforms[0].bTransform);
|
||||
|
||||
// Add cartoon representations
|
||||
await addChainRepresentation(plugin, struct1.structure, chain1, label1, color1);
|
||||
await addChainRepresentation(plugin, struct2.structure, chain2, label2, color2);
|
||||
|
||||
console.log('RMSD:', transforms[0].rmsd.toFixed(2), 'A');
|
||||
|
||||
return { rmsd: transforms[0].rmsd };
|
||||
}
|
||||
39
src/examples/basic-wrapper/tm-align.html
Normal file
39
src/examples/basic-wrapper/tm-align.html
Normal file
@@ -0,0 +1,39 @@
|
||||
<!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>TM-align Superposition</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
// Initialize and automatically run TM-align superposition
|
||||
BasicMolStarWrapper.init('app').then(() => {
|
||||
BasicMolStarWrapper.setBackground(0xffffff);
|
||||
BasicMolStarWrapper.tests.tmAlignSuperposition();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
async function getMappings(id: string) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
(async function () {
|
||||
|
||||
@@ -30,6 +30,7 @@ import { createProteopediaCustomTheme } from './coloring';
|
||||
import { LoadParams, ModelInfo, RepresentationStyle, StateElements, SupportedFormats } from './helpers';
|
||||
import './index.html';
|
||||
import { volumeStreamingControls } from './ui/controls';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
require('../../mol-plugin-ui/skin/light.scss');
|
||||
|
||||
class MolStarProteopediaWrapper {
|
||||
@@ -267,7 +268,7 @@ class MolStarProteopediaWrapper {
|
||||
...trackball,
|
||||
animate: trackball.animate.name === 'spin'
|
||||
? { name: 'off', params: {} }
|
||||
: { name: 'spin', params: { speed: 1 } }
|
||||
: { name: 'spin', params: { speed: 0.1, axis: Vec3.create(0, -1, 0) } }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -118,6 +118,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
@@ -151,6 +152,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,32 +18,33 @@ import { TransformData } from '../../mol-geo/geometry/transform-data';
|
||||
import { sphereVertexCount } from '../../mol-geo/primitive/sphere';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Geometry } from '../../mol-geo/geometry/geometry';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DebugHelperParams = {
|
||||
export const BoundingSphereHelperParams = {
|
||||
sceneBoundingSpheres: PD.Boolean(false, { description: 'Show full scene bounding spheres.' }),
|
||||
visibleSceneBoundingSpheres: PD.Boolean(false, { description: 'Show visible scene bounding spheres.' }),
|
||||
objectBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible render objects.' }),
|
||||
instanceBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible instances.' }),
|
||||
};
|
||||
export type DebugHelperParams = typeof DebugHelperParams
|
||||
export type DebugHelperProps = PD.Values<DebugHelperParams>
|
||||
export type BoundingSphereHelperParams = typeof BoundingSphereHelperParams;
|
||||
export type BoundingSphereHelperProps = PD.Values<BoundingSphereHelperParams>;
|
||||
|
||||
type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
|
||||
|
||||
export class BoundingSphereHelper {
|
||||
export class BoundingSphereHelper implements DebugHelper<BoundingSphereHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DebugHelperProps;
|
||||
private _props: BoundingSphereHelperProps;
|
||||
private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private sceneData: BoundingSphereData | undefined;
|
||||
private visibleSceneData: BoundingSphereData | undefined;
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<BoundingSphereHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DebugHelperParams), ...props };
|
||||
this._props = { ...PD.getDefaultValues(BoundingSphereHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -120,9 +121,9 @@ export class BoundingSphereHelper {
|
||||
this._props.objectBoundingSpheres || this._props.instanceBoundingSpheres
|
||||
);
|
||||
}
|
||||
get props() { return this._props as Readonly<DebugHelperProps>; }
|
||||
get props() { return this._props as Readonly<BoundingSphereHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DebugHelperProps>) {
|
||||
setProps(props: Partial<BoundingSphereHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
@@ -162,4 +163,4 @@ const instanceMaterialId = getNextMaterialId();
|
||||
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1, transform);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
}
|
||||
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Box } from '../../mol-geo/primitive/box';
|
||||
import { Plane } from '../../mol-geo/primitive/plane';
|
||||
import { Cylinder } from '../../mol-geo/primitive/cylinder';
|
||||
import { Sphere } from '../../mol-geo/primitive/sphere';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ClipObjectHelperParams = {
|
||||
clipObjects: PD.Boolean(false, { description: 'Show clip-objects of visible render objects.' }),
|
||||
};
|
||||
export type ClipObjectHelperParams = typeof ClipObjectHelperParams;
|
||||
export type ClipObjectHelperProps = PD.Values<ClipObjectHelperParams>;
|
||||
|
||||
//
|
||||
|
||||
/** Serializes clip object params to a string key for deduplication */
|
||||
function clipObjectKey(type: number, invert: boolean, position: ArrayLike<number>, posOffset: number, rotation: ArrayLike<number>, rotOffset: number, scale: ArrayLike<number>, scaleOffset: number, transform: ArrayLike<number>, transformOffset: number): string {
|
||||
// Round floats to 5 decimal places to avoid floating point noise
|
||||
const r = (v: number) => Math.round(v * 100000) / 100000;
|
||||
const parts = [
|
||||
type, invert ? 1 : 0,
|
||||
r(position[posOffset]), r(position[posOffset + 1]), r(position[posOffset + 2]),
|
||||
r(rotation[rotOffset]), r(rotation[rotOffset + 1]), r(rotation[rotOffset + 2]), r(rotation[rotOffset + 3]),
|
||||
r(scale[scaleOffset]), r(scale[scaleOffset + 1]), r(scale[scaleOffset + 2]),
|
||||
];
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
parts.push(r(transform[transformOffset + j]));
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
type ClipObjectData = {
|
||||
key: string,
|
||||
renderObject: GraphicsRenderObject,
|
||||
indicatorRenderObject: GraphicsRenderObject,
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
const clipObjectColors: Record<number, Color> = {
|
||||
[Clip.Type.plane]: ColorNames.orange,
|
||||
[Clip.Type.sphere]: ColorNames.green,
|
||||
[Clip.Type.cube]: ColorNames.dodgerblue,
|
||||
[Clip.Type.cylinder]: ColorNames.gold,
|
||||
[Clip.Type.infiniteCone]: ColorNames.crimson,
|
||||
};
|
||||
|
||||
const clipMaterialId = getNextMaterialId();
|
||||
const indicatorMaterialId = getNextMaterialId();
|
||||
|
||||
// Pre-rotation matrices for aligning primitives to GLSL SDF local frames
|
||||
// Plane: Rx(-90°) maps primitive Z-normal to GLSL Y-normal
|
||||
const preRotPlaneQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), -Math.PI / 2);
|
||||
const preRotPlaneMat = Mat4.fromQuat(Mat4(), preRotPlaneQuat);
|
||||
// Cone: Rx(+90°) maps primitive Y-axis to GLSL Z-axis
|
||||
const preRotConeQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), Math.PI / 2);
|
||||
const preRotConeMat = Mat4.fromQuat(Mat4(), preRotConeQuat);
|
||||
|
||||
// Temp variables for constructing transforms
|
||||
const _position = Vec3();
|
||||
const _rotation = Quat();
|
||||
const _scale = Vec3();
|
||||
const _clipTransform = Mat4();
|
||||
const _invClipTransform = Mat4();
|
||||
const _rotMat = Mat4();
|
||||
const _translateMat = Mat4();
|
||||
const _baseMat = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
const _axisEnd = Vec3();
|
||||
const _yAxis = Vec3.create(0, 1, 0);
|
||||
const _zAxis = Vec3.create(0, 0, 1);
|
||||
const _indicatorPos = Vec3();
|
||||
|
||||
export class ClipObjectHelper implements DebugHelper<ClipObjectHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ClipObjectHelperProps;
|
||||
private objectsData = new Map<string, ClipObjectData>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ClipObjectHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ClipObjectHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const currentKeys = new Set<string>();
|
||||
const sceneRadius = this.parent.boundingSphereVisible.radius || 50;
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
|
||||
const count = ro.values.dClipObjectCount.ref.value;
|
||||
if (count === 0) return;
|
||||
|
||||
const types = ro.values.uClipObjectType.ref.value;
|
||||
const inverts = ro.values.uClipObjectInvert.ref.value;
|
||||
const positions = ro.values.uClipObjectPosition.ref.value;
|
||||
const rotations = ro.values.uClipObjectRotation.ref.value;
|
||||
const scales = ro.values.uClipObjectScale.ref.value;
|
||||
const transforms = ro.values.uClipObjectTransform.ref.value;
|
||||
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const type = types[i];
|
||||
if (type === Clip.Type.none) continue;
|
||||
|
||||
const key = clipObjectKey(
|
||||
type, inverts[i],
|
||||
positions, i * 3,
|
||||
rotations, i * 4,
|
||||
scales, i * 3,
|
||||
transforms, i * 16
|
||||
);
|
||||
|
||||
currentKeys.add(key);
|
||||
|
||||
if (this.objectsData.has(key)) continue;
|
||||
|
||||
// Extract per-object params
|
||||
Vec3.fromArray(_position, positions, i * 3);
|
||||
Quat.fromArray(_rotation, rotations, i * 4);
|
||||
Quat.normalize(_rotation, _rotation); // ensure unit quaternion for proper rotation
|
||||
Vec3.fromArray(_scale, scales, i * 3);
|
||||
Mat4.fromArray(_clipTransform, transforms, i * 16);
|
||||
|
||||
// Build base transform (translate * rotate) without scale,
|
||||
// so each shape can insert pre-rotations before scale.
|
||||
Mat4.fromQuat(_rotMat, _rotation);
|
||||
Mat4.fromTranslation(_translateMat, _position);
|
||||
Mat4.mul(_baseMat, _translateMat, _rotMat);
|
||||
|
||||
// apply inverse of clip transform
|
||||
if (!Mat4.isIdentity(_clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, _clipTransform);
|
||||
Mat4.mul(_baseMat, _invClipTransform, _baseMat);
|
||||
}
|
||||
|
||||
const mesh = createClipObjectMesh(type, _baseMat, _scale, sceneRadius);
|
||||
const color = clipObjectColors[type] || ColorNames.white;
|
||||
const renderObject = createClipObjectRenderObject(mesh, color, clipMaterialId, type);
|
||||
|
||||
// Create position/rotation indicator mesh
|
||||
const invert = inverts[i];
|
||||
const indicatorMesh = createIndicatorMesh(_position, _rotation, _clipTransform, _scale, type, invert);
|
||||
const indicatorRenderObject = createIndicatorRenderObject(indicatorMesh, indicatorMaterialId);
|
||||
|
||||
this.scene.add(renderObject);
|
||||
this.scene.add(indicatorRenderObject);
|
||||
this.objectsData.set(key, { key, renderObject, indicatorRenderObject, mesh });
|
||||
}
|
||||
});
|
||||
|
||||
// Remove clip objects no longer present
|
||||
this.objectsData.forEach((data, key) => {
|
||||
if (!currentKeys.has(key)) {
|
||||
this.scene.remove(data.renderObject);
|
||||
this.scene.remove(data.indicatorRenderObject);
|
||||
this.objectsData.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.clipObjects;
|
||||
this.objectsData.forEach(data => {
|
||||
data.renderObject.state.visible = visible;
|
||||
data.indicatorRenderObject.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.objectsData.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.clipObjects;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ClipObjectHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ClipObjectHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createClipObjectMesh(type: number, baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
switch (type) {
|
||||
case Clip.Type.plane: return createPlaneMesh(baseMat, sceneRadius);
|
||||
case Clip.Type.sphere: return createSphereMesh(baseMat, scale);
|
||||
case Clip.Type.cube: return createCubeMesh(baseMat, scale);
|
||||
case Clip.Type.cylinder: return createCylinderMesh(baseMat, scale);
|
||||
case Clip.Type.infiniteCone: return createConeMesh(baseMat, scale, sceneRadius);
|
||||
default: return createSphereMesh(baseMat, scale); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plane: GLSL normal is quaternionTransform(rotation, vec3(0,1,0)) — Y-up default.
|
||||
* Plane() primitive lies in XY with normal (0,0,1) along Z.
|
||||
* Pre-rotate Rx(-90°) to align primitive Z-normal to GLSL Y-normal.
|
||||
* Sized to cover the scene bounding sphere. Clip scale is ignored (plane is infinite in GLSL).
|
||||
*/
|
||||
function createPlaneMesh(baseMat: Mat4, sceneRadius: number): Mesh {
|
||||
const size = Math.max(sceneRadius * 2, 10);
|
||||
// baseMat * preRotPlane * uniformScale(size)
|
||||
Mat4.mul(_tmpMat, baseMat, preRotPlaneMat);
|
||||
Mat4.scale(_tmpMat, _tmpMat, Vec3.create(size, size, 1));
|
||||
|
||||
const plane = Plane();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, plane);
|
||||
// Add flipped backface for double-sided visibility
|
||||
MeshBuilder.addPrimitiveFlipped(builderState, _tmpMat, plane);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphere: SDF uses scale * 0.5 as the radii (ellipsoid).
|
||||
* Sphere primitive has radius 1.
|
||||
* Transform: baseMat * scale * 0.5
|
||||
*/
|
||||
function createSphereMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const detail = 2;
|
||||
const sphere = getSphereForHelper(detail);
|
||||
// baseMat * scale(scale * 0.5)
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5));
|
||||
|
||||
const vertexCount = 10 * Math.pow(2, 2 * detail) + 2;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, sphere);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
let _helperSphere: ReturnType<typeof Sphere> | undefined;
|
||||
function getSphereForHelper(detail: number) {
|
||||
if (!_helperSphere) _helperSphere = Sphere(detail);
|
||||
return _helperSphere;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cube: SDF uses scale * 0.5 as half-extents.
|
||||
* Box() primitive is ±0.5 (unit cube), so scaling by `scale` gives half-extents of scale*0.5.
|
||||
*/
|
||||
function createCubeMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
// baseMat * scale(scale)
|
||||
Mat4.scale(_tmpMat, baseMat, scale);
|
||||
|
||||
const box = Box();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, box);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cylinder: SDF axis along Y, radius = scale.x * 0.5, half-height = scale.y * 0.5.
|
||||
* Cylinder primitive: axis along Y, radius=1 in XZ, half-height=0.5 in Y.
|
||||
* Need: X/Z *= scale.x * 0.5 (radius 1 → scale.x*0.5), Y *= scale.y (half-height 0.5 → scale.y*0.5).
|
||||
*/
|
||||
function createCylinderMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const cyl = Cylinder({ radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: true, bottomCap: true });
|
||||
// baseMat * scale(scale.x * 0.5, scale.y, scale.x * 0.5) — use scale.x for both radial axes
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1], scale[0] * 0.5));
|
||||
|
||||
const vertexCount = cyl.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cyl);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* InfiniteCone: GLSL SDF axis along Z, radial in XY.
|
||||
* surface: size.x * length(t.xy) + size.y * t.z = 0 (size = scale * 0.5)
|
||||
* half-angle: tan(θ) = scale.y / scale.x
|
||||
* apex at clip position (origin), opens in -Z direction.
|
||||
*
|
||||
* Cylinder primitive (radiusTop=0, radiusBottom=1, height=1):
|
||||
* axis along Y, tip at Y=+0.5, base at Y=-0.5, base radius=1.
|
||||
*
|
||||
* Transform chain (right-to-left):
|
||||
* 1. Scale(baseRadius, coneLength, baseRadius): stretch primitive to correct proportions
|
||||
* 2. Translate(0, -0.5*coneLength, 0): move tip from Y=+0.5·cL to Y=0 (apex at origin)
|
||||
* (after scale, tip is at Y=+0.5·cL; shifting by -0.5·cL puts it at Y=0)
|
||||
* 3. preRotCone Rx(+90°): map prim-Y→Z, so cone axis becomes Z, opening in -Z
|
||||
* 4. baseMat: position + rotation of clip object
|
||||
*/
|
||||
function createConeMesh(baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
const cone = Cylinder({ radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: false, bottomCap: true });
|
||||
|
||||
// Visible length of the (infinite) cone, and base radius matching the GLSL half-angle
|
||||
const coneLength = Math.max(sceneRadius * 2, 10);
|
||||
const tanHalfAngle = (scale[1] || 1) / (scale[0] || 1); // tan(θ) = scaleY / scaleX
|
||||
const baseRadius = coneLength * tanHalfAngle;
|
||||
|
||||
// baseMat * preRotCone * Translate(0, -coneLength/2, 0) * Scale(baseRadius, coneLength, baseRadius)
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), Vec3.create(baseRadius, coneLength, baseRadius));
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), Vec3.create(0, -coneLength * 0.5, 0));
|
||||
Mat4.mul(_tmpMat, translateMat, scaleMat);
|
||||
Mat4.mul(_tmpMat, preRotConeMat, _tmpMat);
|
||||
Mat4.mul(_tmpMat, baseMat, _tmpMat);
|
||||
|
||||
const vertexCount = cone.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cone);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createClipObjectRenderObject(mesh: Mesh, color: Color, materialId: number, type: number) {
|
||||
const alpha = type === Clip.Type.plane ? 0.25 : 0.15;
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mesh with a sphere at the clip object position and a cylinder
|
||||
* along the characteristic axis to indicate orientation.
|
||||
*
|
||||
* - Plane/sphere/cube/cylinder: axis = rotated Y (matches GLSL normal/axis direction)
|
||||
* - InfiniteCone: axis = rotated Z (cone axis is Z in local frame)
|
||||
* - Plane with invert: direction is flipped
|
||||
*/
|
||||
function createIndicatorMesh(position: Vec3, rotation: Quat, clipTransform: Mat4, scale: Vec3, type: number, invert: boolean): Mesh {
|
||||
const objectSize = Math.max(scale[0], scale[1], scale[2]);
|
||||
const sphereRadius = Math.max(objectSize * 0.004, 0.01);
|
||||
const cylinderRadius = sphereRadius * 0.4;
|
||||
const axisLength = Math.max(objectSize * 0.1, 2);
|
||||
|
||||
// Transform position by inverse of clipTransform if non-identity
|
||||
Vec3.copy(_indicatorPos, position);
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, clipTransform);
|
||||
Vec3.transformMat4(_indicatorPos, _indicatorPos, _invClipTransform);
|
||||
}
|
||||
|
||||
// Choose the local-frame axis based on clip type
|
||||
const localAxis = type === Clip.Type.infiniteCone ? _zAxis : _yAxis;
|
||||
Vec3.transformQuat(_axisEnd, localAxis, rotation);
|
||||
|
||||
// Cone opens in -Z locally, so negate to point along the cone opening
|
||||
if (type === Clip.Type.infiniteCone) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// For planes, the normal points toward the clipped (removed) side.
|
||||
// Flip so the indicator points toward the non-clipped (kept) geometry.
|
||||
// When inverted, the kept side is the normal side, so don't flip.
|
||||
if (type === Clip.Type.plane && !invert) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// If clipTransform is non-identity, also transform the axis direction
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
// Transform direction (not position) by inverse clipTransform
|
||||
const endWorld = Vec3();
|
||||
Vec3.add(endWorld, position, Vec3.scale(Vec3(), _axisEnd, axisLength));
|
||||
Vec3.transformMat4(endWorld, endWorld, _invClipTransform);
|
||||
Vec3.sub(_axisEnd, endWorld, _indicatorPos);
|
||||
Vec3.normalize(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// Axis cylinder endpoint
|
||||
const axisEndPoint = Vec3();
|
||||
Vec3.scaleAndAdd(axisEndPoint, _indicatorPos, _axisEnd, axisLength);
|
||||
|
||||
const builderState = MeshBuilder.createState(512, 256);
|
||||
// Position sphere
|
||||
addSphere(builderState, _indicatorPos, sphereRadius, 1);
|
||||
// Rotation axis cylinder
|
||||
addCylinder(builderState, _indicatorPos, axisEndPoint, 1, { radiusTop: cylinderRadius, radiusBottom: cylinderRadius, radialSegments: 8 });
|
||||
// Small sphere at tip of axis
|
||||
addSphere(builderState, axisEndPoint, cylinderRadius * 1.5, 1);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createIndicatorRenderObject(mesh: Mesh, materialId: number) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.7, doubleSided: false, cellSize: 0, batchSize: 0 }, ColorNames.white, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { DirectVolumeValues } from '../../mol-gl/renderable/direct-volume';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DirectVolumeHelperParams = {
|
||||
directVolumeEdges: PD.Boolean(false, { description: 'Show edges of visible direct-volume render objects.' }),
|
||||
};
|
||||
export type DirectVolumeHelperParams = typeof DirectVolumeHelperParams;
|
||||
export type DirectVolumeHelperProps = PD.Values<DirectVolumeHelperParams>;
|
||||
|
||||
const directVolumeMaterialId = getNextMaterialId();
|
||||
|
||||
type TrackedEntry = { ro: GraphicsRenderObject, version: number };
|
||||
|
||||
export class DirectVolumeHelper implements DebugHelper<DirectVolumeHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DirectVolumeHelperProps;
|
||||
private renderObjects = new Map<number, TrackedEntry>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DirectVolumeHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DirectVolumeHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'direct-volume') return;
|
||||
|
||||
const values = ro.values as DirectVolumeValues;
|
||||
const version = values.uUnitToCartn.ref.version + values.uGridDim.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entry if version changed
|
||||
if (existing) {
|
||||
this.scene.remove(existing.ro);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const lines = createVolumeEdgeLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createLinesRenderObject(lines, directVolumeMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, { ro: linesRO, version });
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
this.scene.remove(entry.ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.directVolumeEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
entry.ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.directVolumeEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<DirectVolumeHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DirectVolumeHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* The volume proxy box in the shader uses aPosition in [-0.5, 0.5]^3,
|
||||
* shifted to [0,1]^3 (unitCoord = aPosition + 0.5), then transformed by:
|
||||
* uUnitToCartn → Cartesian space
|
||||
* aTransform → instance space
|
||||
*
|
||||
* We replicate this pipeline to get the correct world-space edges.
|
||||
* Grid ticks are placed at 1/gridDim intervals along each edge.
|
||||
*/
|
||||
function createVolumeEdgeLines(values: DirectVolumeValues): Lines | undefined {
|
||||
const unitToCartn = values.uUnitToCartn.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
if (bs.radius < 1e-6) return undefined;
|
||||
|
||||
const builder = LinesBuilder.create(128 * instanceCount);
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
// Combined transform: aTransform * uUnitToCartn
|
||||
const combined = Mat4.mul(Mat4(), instTransform, unitToCartn);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.8 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
266
src/extensions/debug-helpers/image-helper.ts
Normal file
266
src/extensions/debug-helpers/image-helper.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { ImageValues } from '../../mol-gl/renderable/image';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { addSphere as addLinesSphere } from '../../mol-geo/geometry/lines/builder/sphere';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { addPlane } from '../../mol-geo/geometry/lines/builder/plane';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ImageHelperParams = {
|
||||
imageEdges: PD.Boolean(false, { description: 'Show edges of visible image render objects.' }),
|
||||
};
|
||||
export type ImageHelperParams = typeof ImageHelperParams;
|
||||
export type ImageHelperProps = PD.Values<ImageHelperParams>;
|
||||
|
||||
const imageEdgeMaterialId = getNextMaterialId();
|
||||
const imageTrimMaterialId = getNextMaterialId();
|
||||
|
||||
// Temp vectors
|
||||
const _trimPos = Vec3();
|
||||
const _trimScale = Vec3();
|
||||
const _trimRot = Quat();
|
||||
const _trimTransform = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
|
||||
export class ImageHelper implements DebugHelper<ImageHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ImageHelperProps;
|
||||
private renderObjects = new Map<number, { roList: GraphicsRenderObject[], version: number }>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ImageHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ImageHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'image') return;
|
||||
|
||||
const values = ro.values as ImageValues;
|
||||
const version = values.aPosition.ref.version
|
||||
+ values.uTrimType.ref.version + values.uTrimCenter.ref.version
|
||||
+ values.uTrimRotation.ref.version + values.uTrimScale.ref.version
|
||||
+ values.uTrimTransform.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entries if version changed
|
||||
if (existing) {
|
||||
for (const oldRO of existing.roList) this.scene.remove(oldRO);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const roList: GraphicsRenderObject[] = [];
|
||||
|
||||
const edgeLines = createImageEdgeLines(values);
|
||||
if (edgeLines) {
|
||||
const edgeRO = createLinesRenderObject(edgeLines, imageEdgeMaterialId, ColorNames.cyan, 0.8);
|
||||
this.scene.add(edgeRO);
|
||||
roList.push(edgeRO);
|
||||
}
|
||||
|
||||
const trimLines = createTrimEdgeLines(values);
|
||||
if (trimLines) {
|
||||
const trimRO = createLinesRenderObject(trimLines, imageTrimMaterialId, ColorNames.yellow, 0.7);
|
||||
this.scene.add(trimRO);
|
||||
roList.push(trimRO);
|
||||
}
|
||||
|
||||
if (roList.length > 0) {
|
||||
this.renderObjects.set(ro.id, { roList, version });
|
||||
}
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
for (const ro of entry.roList) this.scene.remove(ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.imageEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
for (const ro of entry.roList) ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.imageEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ImageHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ImageHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* Image quad vertex layout (from image.ts):
|
||||
* Vertex 0: UV (0,1) — top-left
|
||||
* Vertex 1: UV (0,0) — bottom-left
|
||||
* Vertex 2: UV (1,1) — top-right
|
||||
* Vertex 3: UV (1,0) — bottom-right
|
||||
*
|
||||
* addPlane expects corners in winding order (0→1→2→3→0),
|
||||
* so we reorder to: top-left, bottom-left, bottom-right, top-right.
|
||||
*/
|
||||
const _planeCorners = new Float32Array(12);
|
||||
|
||||
function createImageEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
if (positions.length < 12) return undefined; // need 4 vertices × 3 components
|
||||
|
||||
// Reorder from [TL, BL, TR, BR] to winding order [TL, BL, BR, TR]
|
||||
// V0 (TL) → slot 0
|
||||
_planeCorners[0] = positions[0]; _planeCorners[1] = positions[1]; _planeCorners[2] = positions[2];
|
||||
// V1 (BL) → slot 1
|
||||
_planeCorners[3] = positions[3]; _planeCorners[4] = positions[4]; _planeCorners[5] = positions[5];
|
||||
// V3 (BR) → slot 2
|
||||
_planeCorners[6] = positions[9]; _planeCorners[7] = positions[10]; _planeCorners[8] = positions[11];
|
||||
// V2 (TR) → slot 3
|
||||
_planeCorners[9] = positions[6]; _planeCorners[10] = positions[7]; _planeCorners[11] = positions[8];
|
||||
|
||||
const builder = LinesBuilder.create(4 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, inst * 16);
|
||||
addPlane(builder, _planeCorners, transform, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createTrimEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const trimType = values.uTrimType.ref.value as number;
|
||||
if (trimType === 0) return undefined; // no trim
|
||||
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
Vec3.copy(_trimPos, values.uTrimCenter.ref.value);
|
||||
Quat.copy(_trimRot, values.uTrimRotation.ref.value);
|
||||
Vec3.copy(_trimScale, values.uTrimScale.ref.value);
|
||||
Mat4.copy(_trimTransform, values.uTrimTransform.ref.value);
|
||||
|
||||
if (trimType === Clip.Type.cube) {
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
} else if (trimType === Clip.Type.sphere) {
|
||||
return createSphereTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
// For other trim types (plane, cylinder, cone), draw a cube outline as a fallback
|
||||
// using the trim center/scale/rotation
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
function createCubeTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
// Build cube transform: translate * rotate * scale
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), _trimScale);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
Mat4.mul(_tmpMat, _tmpMat, scaleMat);
|
||||
|
||||
// Apply inverse of trim transform
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
// addBox uses [0,1]^3, trim cube uses [-0.5,0.5]^3 — prepend offset
|
||||
const offset = Mat4.fromTranslation(Mat4(), Vec3.create(-0.5, -0.5, -0.5));
|
||||
Mat4.mul(_tmpMat, _tmpMat, offset);
|
||||
|
||||
const builder = LinesBuilder.create(12 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createSphereTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
const radius = Math.max(_trimScale[0] * 0.5, _trimScale[1] * 0.5, _trimScale[2] * 0.5);
|
||||
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
const segments = 32;
|
||||
const circlesPerDimension = 3;
|
||||
const builder = LinesBuilder.create(segments * 3 * circlesPerDimension * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
|
||||
addLinesSphere(builder, radius, combined, 0, { segments, circlesPerDimension });
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number, color: Color, alpha: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, color, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
71
src/extensions/debug-helpers/index.ts
Normal file
71
src/extensions/debug-helpers/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, BoundingSphereHelperParams } from './bounding-sphere-helper';
|
||||
import { ClipObjectHelper, ClipObjectHelperParams } from './clip-object-helper';
|
||||
import { DirectVolumeHelper, DirectVolumeHelperParams } from './direct-volume-helper';
|
||||
import { ImageHelper, ImageHelperParams } from './image-helper';
|
||||
import { MeshHelper, MeshHelperParams } from './mesh-helper';
|
||||
|
||||
const DebugHelpersParams = {
|
||||
...BoundingSphereHelperParams,
|
||||
...ClipObjectHelperParams,
|
||||
...MeshHelperParams,
|
||||
...ImageHelperParams,
|
||||
...DirectVolumeHelperParams,
|
||||
};
|
||||
type DebugHelpersParams = typeof DebugHelpersParams;
|
||||
type DebugHelpersProps = PD.Values<DebugHelpersParams>;
|
||||
|
||||
export const DebugHelpers = PluginBehavior.create<DebugHelpersProps>({
|
||||
name: 'extension-debug-helpers',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Debug Helpers'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<DebugHelpersProps> {
|
||||
async register(): Promise<void> {
|
||||
await this.ctx.canvas3dInitialized;
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
const { ctx, parent } = dr;
|
||||
|
||||
dr.register('bounding-sphere', new BoundingSphereHelper(ctx, parent, this.params));
|
||||
dr.register('clip-object', new ClipObjectHelper(ctx, parent, this.params));
|
||||
dr.register('mesh', new MeshHelper(ctx, parent, this.params));
|
||||
dr.register('image', new ImageHelper(ctx, parent, this.params));
|
||||
dr.register('direct-volume', new DirectVolumeHelper(ctx, parent, this.params));
|
||||
}
|
||||
|
||||
update(params: DebugHelpersProps) {
|
||||
const changed = super.update(params);
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (changed && canvas3d) {
|
||||
canvas3d.debugRegistry.setProps(params);
|
||||
canvas3d.requestDraw();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
dr.unregister('bounding-sphere');
|
||||
dr.unregister('clip-object');
|
||||
dr.unregister('mesh');
|
||||
dr.unregister('image');
|
||||
dr.unregister('direct-volume');
|
||||
}
|
||||
},
|
||||
params: () => DebugHelpersParams,
|
||||
canAutoUpdate: () => true,
|
||||
});
|
||||
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { MeshValues } from '../../mol-gl/renderable/mesh';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const MeshHelperParams = {
|
||||
meshNormals: PD.Boolean(false, { description: 'Show normals of visible mesh render objects.' }),
|
||||
};
|
||||
export type MeshHelperParams = typeof MeshHelperParams;
|
||||
export type MeshHelperProps = PD.Values<MeshHelperParams>;
|
||||
|
||||
const meshHelperMaterialId = getNextMaterialId();
|
||||
|
||||
const _v = Vec3();
|
||||
const _n = Vec3();
|
||||
const _start = Vec3();
|
||||
const _end = Vec3();
|
||||
|
||||
export class MeshHelper implements DebugHelper<MeshHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: MeshHelperProps;
|
||||
private renderObjects = new Map<number, GraphicsRenderObject>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<MeshHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(MeshHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
const currentIds = new Set<number>();
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'mesh') return;
|
||||
|
||||
currentIds.add(ro.id);
|
||||
|
||||
// Skip if we already have normals for this render object
|
||||
if (this.renderObjects.has(ro.id)) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = ro.values as MeshValues;
|
||||
const lines = createNormalLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createNormalLinesRenderObject(lines, meshHelperMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, linesRO);
|
||||
});
|
||||
|
||||
// Remove normals for render objects no longer present
|
||||
for (const id of previousIds) {
|
||||
const linesRO = this.renderObjects.get(id);
|
||||
if (linesRO) {
|
||||
this.scene.remove(linesRO);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.meshNormals;
|
||||
this.renderObjects.forEach(ro => {
|
||||
ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.meshNormals;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<MeshHelperProps>; }
|
||||
|
||||
setProps(props: Partial<MeshHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createNormalLines(values: MeshValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const normals = values.aNormal.ref.value;
|
||||
const indices = values.elements.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const vertexCount = positions.length / 3;
|
||||
if (vertexCount === 0) return undefined;
|
||||
|
||||
// Determine normal line length: proportional to bounding sphere radius
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
const normalLength = Math.max(bs.radius * 0.01, 0.1);
|
||||
|
||||
// Count unique vertices referenced by indices
|
||||
const indexCount = values.drawCount.ref.value;
|
||||
|
||||
const builder = LinesBuilder.create(indexCount * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const tOffset = inst * 16;
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, tOffset);
|
||||
|
||||
// Use a set to avoid drawing duplicate normals for shared vertices
|
||||
const visited = new Set<number>();
|
||||
|
||||
for (let i = 0; i < indexCount; ++i) {
|
||||
const vi = indices[i];
|
||||
if (visited.has(vi)) continue;
|
||||
visited.add(vi);
|
||||
|
||||
const vo = vi * 3;
|
||||
Vec3.set(_v, positions[vo], positions[vo + 1], positions[vo + 2]);
|
||||
Vec3.set(_n, normals[vo], normals[vo + 1], normals[vo + 2]);
|
||||
|
||||
// Transform vertex position and normal direction by instance transform
|
||||
Vec3.transformMat4(_start, _v, transform);
|
||||
Vec3.transformDirection(_end, _n, transform);
|
||||
Vec3.normalize(_end, _end);
|
||||
Vec3.scaleAndAdd(_end, _start, _end, normalLength);
|
||||
|
||||
builder.addVec(_start, _end, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createNormalLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.7 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.magenta, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, url
|
||||
|
||||
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' }));
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -241,7 +241,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a glTF mesh if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers || !sameColorBuffer) {
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = GlbExporter.getInstance(input, instanceIndex);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = GlbExporter.getInstance(input, instanceIndex);
|
||||
|
||||
// create geometry buffers if needed
|
||||
if (instanceIndex === 0 || !sameGeometryBuffers) {
|
||||
@@ -253,7 +253,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a color buffer if needed
|
||||
if (instanceIndex === 0 || !sameColorBuffer) {
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
}
|
||||
|
||||
// glTF mesh
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -30,6 +30,10 @@ import { RenderObjectExporter, RenderObjectExportData } from './render-object-ex
|
||||
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
|
||||
import { assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { computeFrenetFrames } from '../../mol-math/linear-algebra/3d/frenet-frames';
|
||||
import { addTube } from '../../mol-geo/geometry/mesh/builder/tube';
|
||||
import { arrayCopyOffset } from '../../mol-util/array';
|
||||
|
||||
const GeoExportName = 'geo-export';
|
||||
|
||||
@@ -49,22 +53,32 @@ export interface AddMeshInput {
|
||||
groups: Float32Array | Uint8Array
|
||||
vertexCount: number
|
||||
drawCount: number
|
||||
vertexMapping?: number[]
|
||||
} | undefined
|
||||
meshes: Mesh[] | undefined
|
||||
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
|
||||
values: BaseValues & {
|
||||
readonly uDoubleSided?: ValueCell<boolean>
|
||||
readonly aGroup?: ValueCell<Float32Array>
|
||||
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
|
||||
}
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
webgl: WebGLContext | undefined
|
||||
ctx: RuntimeContext
|
||||
vertexMapping?: number[]
|
||||
}
|
||||
|
||||
export type MeshGeoData = {
|
||||
values: BaseValues,
|
||||
groups: Float32Array | Uint8Array,
|
||||
vertexCount: number,
|
||||
instanceIndex: number,
|
||||
isGeoTexture: boolean,
|
||||
values: BaseValues & {
|
||||
readonly aGroup?: ValueCell<Float32Array>
|
||||
readonly tPositionGroup?: ValueCell<TextureImage<Float32Array>>
|
||||
}
|
||||
groups?: Float32Array | Uint8Array,
|
||||
vertexCount: number
|
||||
instanceIndex: number
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
vertexMapping?: number[]
|
||||
}
|
||||
|
||||
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
|
||||
@@ -77,7 +91,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
return unpackRGBToInt(r, g, b) / sizeDataFactor;
|
||||
}
|
||||
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number): number {
|
||||
private static getSize(values: BaseValues & SizeValues, instanceIndex: number, group: number, vertexIndex: number): number {
|
||||
const tSize = values.tSize.ref.value;
|
||||
let size = 0;
|
||||
switch (values.dSizeType.ref.value) {
|
||||
@@ -94,6 +108,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * groupCount + group);
|
||||
break;
|
||||
case 'vertex':
|
||||
size = MeshExporter.getSizeFromTexture(tSize, vertexIndex);
|
||||
break;
|
||||
case 'vertexInstance':
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
size = MeshExporter.getSizeFromTexture(tSize, instanceIndex * vertexCount + vertexIndex);
|
||||
break;
|
||||
}
|
||||
return size * values.uSizeFactor.ref.value;
|
||||
}
|
||||
@@ -225,13 +246,14 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
indices: mesh.indexBuffer.ref.value,
|
||||
groups: mesh.groupBuffer.ref.value,
|
||||
vertexCount: mesh.vertexCount,
|
||||
drawCount: mesh.triangleCount * 3
|
||||
drawCount: mesh.triangleCount * 3,
|
||||
vertexMapping: input.vertexMapping,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const { values, groups, instanceIndex, isGeoTexture, mode } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const uColor = values.uColor.ref.value;
|
||||
@@ -239,13 +261,23 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const overpaintType = values.dOverpaintType.ref.value;
|
||||
const dOverpaint = values.dOverpaint.ref.value;
|
||||
const tOverpaint = values.tOverpaint.ref.value.array;
|
||||
const usePalette = values.dUsePalette.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
if (geoData.vertexMapping) {
|
||||
vertexIndex = geoData.vertexMapping[vertexIndex];
|
||||
vertexCount = values.uVertexCount.ref.value;
|
||||
} else if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
const group = isGeoTexture
|
||||
? MeshExporter.getGroup(groups!, vertexIndex)
|
||||
: values.dGeometryType.ref.value === 'spheres'
|
||||
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
|
||||
: values.aGroup!.ref.value[vertexIndex];
|
||||
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
@@ -255,12 +287,10 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
color = Color.fromArray(tColor, instanceIndex * 3);
|
||||
break;
|
||||
case 'group': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
color = Color.fromArray(tColor, group * 3);
|
||||
break;
|
||||
}
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
color = Color.fromArray(tColor, (instanceIndex * groupCount + group) * 3);
|
||||
break;
|
||||
}
|
||||
@@ -279,12 +309,31 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
default: throw new Error('Unsupported color type.');
|
||||
}
|
||||
|
||||
if (usePalette) {
|
||||
const palette = values.tPalette.ref.value;
|
||||
const paletteArray = palette.array;
|
||||
const paletteLength = paletteArray.length / 3;
|
||||
const [r, g, b] = Color.toRgb(color);
|
||||
const paletteValue = ((r * 256 * 256 + g * 256 + b) - 1) / ColorTheme.PaletteScale;
|
||||
const fIndex = paletteValue * (paletteLength - 1);
|
||||
if (palette.filter === 'nearest') {
|
||||
const index = Math.round(fIndex);
|
||||
color = Color.fromArray(paletteArray, index * 3);
|
||||
} else { // linear
|
||||
const index0 = Math.floor(fIndex);
|
||||
const index1 = index0 + 1;
|
||||
const t = fIndex - index0;
|
||||
const color0 = Color.fromArray(paletteArray, index0 * 3);
|
||||
const color1 = Color.fromArray(paletteArray, index1 * 3);
|
||||
color = Color.interpolate(color0, color1, t);
|
||||
}
|
||||
}
|
||||
|
||||
if (dOverpaint) {
|
||||
let overpaintColor: Color;
|
||||
let overpaintAlpha: number;
|
||||
switch (overpaintType) {
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
const idx = (instanceIndex * groupCount + group) * 4;
|
||||
overpaintColor = Color.fromArray(tOverpaint, idx);
|
||||
overpaintAlpha = tOverpaint[idx + 3] / 255;
|
||||
@@ -320,16 +369,24 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const transparencyType = values.dTransparencyType.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
if (geoData.vertexMapping) {
|
||||
vertexIndex = geoData.vertexMapping[vertexIndex];
|
||||
vertexCount = values.uVertexCount.ref.value;
|
||||
} else if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
const group = isGeoTexture
|
||||
? MeshExporter.getGroup(groups!, vertexIndex)
|
||||
: values.dGeometryType.ref.value === 'spheres'
|
||||
? values.tPositionGroup!.ref.value.array[vertexIndex * 4 + 3]
|
||||
: values.aGroup!.ref.value[vertexIndex];
|
||||
|
||||
let transparency: number = 0;
|
||||
if (dTransparency) {
|
||||
switch (transparencyType) {
|
||||
case 'groupInstance': {
|
||||
const group = isGeoTexture ? MeshExporter.getGroup(groups, vertexIndex) : groups[vertexIndex];
|
||||
const idx = (instanceIndex * groupCount + group);
|
||||
transparency = tTransparency[idx] / 255;
|
||||
break;
|
||||
@@ -373,10 +430,133 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
private async addLineStrips(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const stripCount = values.stripCount.ref.value;
|
||||
|
||||
const stripOffsets = values.stripOffsets.ref.value;
|
||||
const aMapping = values.aMapping.ref.value;
|
||||
|
||||
if (this.options.linesAsTriangles) {
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
const radialSegments = 6;
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
|
||||
// Collect segments for this strip (only end-side vertices)
|
||||
const segmentIndices: number[] = [];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
segmentIndices.push(v);
|
||||
}
|
||||
if (segmentIndices.length === 0) continue;
|
||||
|
||||
const nPoints = segmentIndices.length + 1;
|
||||
const linearSegments = nPoints - 1;
|
||||
const curvePoints = new Float32Array(nPoints * 3);
|
||||
const curveOrigIndices: number[] = [];
|
||||
const widthValues = new Float32Array(nPoints);
|
||||
const heightValues = new Float32Array(nPoints);
|
||||
|
||||
// First point: start of first segment
|
||||
const v0 = segmentIndices[0];
|
||||
arrayCopyOffset(curvePoints, aStart, 0, v0 * 3, 3);
|
||||
curveOrigIndices.push(v0);
|
||||
const radius0 = MeshExporter.getSize(values, instanceIndex, aGroup[v0], v0) * 0.03;
|
||||
widthValues[0] = radius0;
|
||||
heightValues[0] = radius0;
|
||||
|
||||
// Subsequent points: end of each segment
|
||||
for (let j = 0; j < segmentIndices.length; ++j) {
|
||||
const v = segmentIndices[j];
|
||||
arrayCopyOffset(curvePoints, aEnd, (j + 1) * 3, v * 3, 3);
|
||||
curveOrigIndices.push(v);
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, aGroup[v], v) * 0.03;
|
||||
widthValues[j + 1] = radius;
|
||||
heightValues[j + 1] = radius;
|
||||
}
|
||||
|
||||
const normalVectors = new Float32Array(nPoints * 3);
|
||||
const binormalVectors = new Float32Array(nPoints * 3);
|
||||
computeFrenetFrames(curvePoints, normalVectors, binormalVectors, nPoints);
|
||||
|
||||
addTube(state, curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, widthValues, heightValues, true, true, 'elliptical');
|
||||
|
||||
// Build vertex mapping
|
||||
if (instanceIndex === 0) {
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[i]);
|
||||
}
|
||||
}
|
||||
for (let j = 0; j <= radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[0]);
|
||||
}
|
||||
for (let j = 0; j <= radialSegments; ++j) {
|
||||
vertexMapping.push(curveOrigIndices[linearSegments]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
// Decompose strips into individual line segments
|
||||
let nLineSegments = 0;
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
nLineSegments++;
|
||||
}
|
||||
}
|
||||
|
||||
const vertexCount = nLineSegments * 2;
|
||||
const drawCount = nLineSegments;
|
||||
const vertices = new Float32Array(vertexCount * 3);
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
let vertexIndex = 0;
|
||||
for (let s = 0; s < stripCount; ++s) {
|
||||
const stripStart = stripOffsets[s];
|
||||
const stripEnd = stripOffsets[s + 1];
|
||||
for (let v = stripStart; v < stripEnd; v += 2) {
|
||||
const mappingY = aMapping[v * 2 + 1];
|
||||
if (mappingY < 0) continue;
|
||||
|
||||
arrayCopyOffset(vertices, aStart, vertexIndex * 3, v * 3, 3);
|
||||
vertexMapping[vertexIndex] = v;
|
||||
vertexIndex++;
|
||||
|
||||
arrayCopyOffset(vertices, aEnd, vertexIndex * 3, v * 3, 3);
|
||||
vertexMapping[vertexIndex] = v;
|
||||
vertexIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount, vertexMapping }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addLineSegments(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
|
||||
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
|
||||
const drawCount = values.drawCount.ref.value / (2 * 3);
|
||||
|
||||
@@ -391,6 +571,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const topCap = true;
|
||||
const bottomCap = true;
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -398,35 +580,44 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
const group = aGroup[i / 4];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i / 4) * 0.03;
|
||||
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
const n = vertexCount / 2;
|
||||
const vertices = new Float32Array(n * 2 * 3);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
vertices[i * 6] = aStart[i * 4 * 3];
|
||||
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
|
||||
|
||||
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
|
||||
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
|
||||
arrayCopyOffset(vertices, aStart, i * 6, i * 4 * 3, 3);
|
||||
arrayCopyOffset(vertices, aEnd, i * 6 + 3, i * 4 * 3, 3);
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (values.stripCount.ref.value !== 0) {
|
||||
await this.addLineStrips(values, webgl, ctx);
|
||||
} else {
|
||||
await this.addLineSegments(values, webgl, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
@@ -440,6 +631,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const detail = 0;
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
@@ -448,15 +640,21 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
state.currentGroup = group;
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * 0.03;
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addSphere(state, center, radius, detail);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
} else {
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
|
||||
}
|
||||
@@ -471,7 +669,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const sphereCount = vertexCount / 6 * instanceCount;
|
||||
const sphereCount = (vertexCount / 6) * instanceCount;
|
||||
let detail: number;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
@@ -492,6 +690,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -499,15 +699,21 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group);
|
||||
state.currentGroup = group;
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i);
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addSphere(state, center, radius, detail);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
@@ -545,6 +751,8 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
const vertexMapping: number[] = [];
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
@@ -554,7 +762,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
v3sub(dir, end, start);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group, i) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
let topCap = cap === 1 || cap === 3;
|
||||
let bottomCap = cap >= 2;
|
||||
@@ -562,14 +770,20 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
[bottomCap, topCap] = [topCap, bottomCap];
|
||||
}
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
const vertexOffset = state.vertices.elementCount;
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
|
||||
if (instanceIndex === 0) {
|
||||
for (let vi = vertexOffset; vi < state.vertices.elementCount; ++vi) {
|
||||
vertexMapping.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx, vertexMapping });
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
|
||||
@@ -107,7 +107,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = ObjExporter.getInstance(input, instanceIndex);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = ObjExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
@@ -137,7 +137,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
|
||||
|
||||
// color
|
||||
const quantizedColors = new Uint8Array(drawCount * 3);
|
||||
|
||||
@@ -100,7 +100,7 @@ def Material "material${materialKey}"
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
if (ctx.shouldUpdate) await ctx.update({ current: instanceIndex + 1 });
|
||||
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount } = UsdzExporter.getInstance(input, instanceIndex);
|
||||
const { vertices, normals, indices, groups, vertexCount, drawCount, vertexMapping } = UsdzExporter.getInstance(input, instanceIndex);
|
||||
|
||||
Mat4.fromArray(t, aTransform, instanceIndex * 16);
|
||||
Mat4.mul(t, this.centerTransform, t);
|
||||
@@ -134,7 +134,7 @@ def Material "material${materialKey}"
|
||||
StringBuilder.writeSafe(normalBuilder, ')');
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode, vertexMapping };
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; ++i) {
|
||||
|
||||
28
src/extensions/kinemage/README.md
Normal file
28
src/extensions/kinemage/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Kinemage extension
|
||||
|
||||
This extension adds support for the Kinemage molecular graphics format based on the
|
||||
[kinemage format specification](http://kinemage.biochem.duke.edu/static/files/PDFs/format-kinemage.pdf).
|
||||
|
||||
It currently supports the following features:
|
||||
- Drag-and-drop of Kinemage files into the display area
|
||||
- Open File can open Kinemage files from the local filesystem
|
||||
- Display of @ball, @sphere, @vector, @dot, @ribbon, and @triangle lists
|
||||
- Coloring of objects by vertex color, or by a single color for the entire list
|
||||
- Hovering over objects to see their labels (if present)
|
||||
- When there are views defined, controls are added to the right panel; when selected, they shift the view
|
||||
- When the view is changes, the projection is set to orthographic and the background is set to black to match Kinemage's default view
|
||||
- Control panel names are based on the @pdbfile or @caption in the Kinemage file if there is one
|
||||
- Lines are split in half, with each half colored by and labeled by the nearest vertex
|
||||
- Master and submaster selections of visible objects
|
||||
- Group and subgroup hierarchy with buttons to control visibility
|
||||
- @pointmaster lists controlling visibility of points
|
||||
- animate/2animate: First entry turned on to start, changing visibility of Animate button cycles through them
|
||||
|
||||
Currently unsupported features include:
|
||||
- @label and @ring lists
|
||||
- @hsvcolor keyword for coloring by hue, saturation, and value
|
||||
- 'fore' and 'rear' keywords for different front and back colors
|
||||
|
||||
Current limitations include:
|
||||
- Triangles are a single color, not colored by vertex (Mol* does not support per-vertex coloring for these primitives)
|
||||
- Line segments in Mol* do not support end-caps for wide lines, so there are artifacts in highly-curved lines
|
||||
70
src/extensions/kinemage/_spec/kin.spec.ts
Normal file
70
src/extensions/kinemage/_spec/kin.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { parseKin } from '../reader/parser';
|
||||
|
||||
const kinString = `@kinemage 1
|
||||
@caption probe.2.26.021123, run Tue Apr 23 14:49:17 2024
|
||||
command: C:\tmp\cctbx_phenix\build\probe\exe\probe.exe -kin -mc -het -once -wat2wat -onlybadout -stdbonds water all 1ssxFH.pdb
|
||||
@group dominant {dots}
|
||||
@subgroup dominant {once dots}
|
||||
@master {bad overlap}
|
||||
@pointmaster 'O' {Hets contacts}
|
||||
@vectorlist {x} color=red master={bad overlap}
|
||||
{ O HOH 319 A}hotpink P 'O' 31.146,32.100,-1.425 {"}hotpink 'O' 31.015,32.234,-1.324
|
||||
{"}hotpink P 'O' 31.607,32.750,-1.156 {"}hotpink 'O' 31.410,32.784,-1.097
|
||||
{"}hotpink P 'O' 31.263,32.074,-1.185 {"}hotpink 'O' 31.117,32.209,-1.122
|
||||
{ O BHOH 338 A}hotpink P 'O' 32.540,45.631,10.833 {"}hotpink 'O' 32.430,45.771,10.977
|
||||
{"}hotpink P 'O' 32.316,45.500,10.828 {"}hotpink 'O' 32.230,45.689,10.998
|
||||
{"}hotpink P 'O' 32.068,45.424,10.824 {"}hotpink 'O' 32.034,45.604,10.975
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
// @todo Replace with more complex kinemage
|
||||
const kinComplexString = `@kinemage 1
|
||||
@caption probe.2.26.021123, run Tue Apr 23 14:49:17 2024
|
||||
command: C:\tmp\cctbx_phenix\build\probe\exe\probe.exe -kin -mc -het -once -wat2wat -onlybadout -stdbonds water all 1ssxFH.pdb
|
||||
@group dominant {dots}
|
||||
@subgroup dominant {once dots}
|
||||
@master {bad overlap}
|
||||
@pointmaster 'O' {Hets contacts}
|
||||
@vectorlist {x} color=red master={bad overlap}
|
||||
{ O HOH 319 A}hotpink P 'O' 31.146,32.100,-1.425 {"}hotpink 'O' 31.015,32.234,-1.324
|
||||
{"}hotpink P 'O' 31.607,32.750,-1.156 {"}hotpink 'O' 31.410,32.784,-1.097
|
||||
{"}hotpink P 'O' 31.263,32.074,-1.185 {"}hotpink 'O' 31.117,32.209,-1.122
|
||||
{ O BHOH 338 A}hotpink P 'O' 32.540,45.631,10.833 {"}hotpink 'O' 32.430,45.771,10.977
|
||||
{"}hotpink P 'O' 32.316,45.500,10.828 {"}hotpink 'O' 32.230,45.689,10.998
|
||||
{"}hotpink P 'O' 32.068,45.424,10.824 {"}hotpink 'O' 32.034,45.604,10.975
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
describe('kin reader', () => {
|
||||
it('basic', async () => {
|
||||
const parsed = await parseKin(kinString).run();
|
||||
if (parsed.isError) return;
|
||||
if (parsed.result.length !== 1) return;
|
||||
const kinemage = parsed.result[0];
|
||||
|
||||
const vectors = kinemage.vectorLists;
|
||||
expect(vectors.length).toEqual(1);
|
||||
|
||||
const element = vectors[0];
|
||||
expect(element.name).toEqual('x');
|
||||
expect(element.position1Array.length).toEqual(7);
|
||||
|
||||
// TODO: Add more tests
|
||||
|
||||
expect.assertions(3);
|
||||
});
|
||||
|
||||
it('complex', async () => {
|
||||
const parsed = await parseKin(kinComplexString).run();
|
||||
if (parsed.isError) return;
|
||||
|
||||
// TODO: Add more complex tests
|
||||
|
||||
});
|
||||
});
|
||||
548
src/extensions/kinemage/behavior.ts
Normal file
548
src/extensions/kinemage/behavior.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { Vec3, Mat3 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { KinemageDataProvider, KinemageData } from './prop';
|
||||
import { StateTransformer, StateBuilder } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginDragAndDropHandler } from '../../mol-plugin-state/manager/drag-and-drop';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/compiler';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { shapePointsFromKin, shapeLinesFromKin, shapeMeshFromKin, shapeSpheresFromKin } from './kin';
|
||||
import { Kinemage } from './reader/schema';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { KinemageControls } from './ui';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
const Tag = KinemageData.Tag;
|
||||
|
||||
const Transform = StateTransformer.builderFactory('sb-kinemage');
|
||||
|
||||
/**
|
||||
* State object to hold parsed Kinemage data
|
||||
*/
|
||||
export class KinemageObject extends PluginStateObject.Create<KinemageData>({ name: 'Kinemage', typeClass: 'Object' }) { }
|
||||
|
||||
/**
|
||||
* Apply a saved snapshot object (from a view state node) to the plugin camera.
|
||||
* Use PluginCommands.Camera.SetSnapshot so transitions and canvas props are handled properly.
|
||||
*/
|
||||
export async function applyViewSnapshot(plugin: PluginContext, snapshot: Partial<Camera.Snapshot>) {
|
||||
if (!snapshot) return;
|
||||
|
||||
// Set background color to black
|
||||
plugin.canvas3d?.setProps({
|
||||
renderer: {
|
||||
...plugin.canvas3d.props.renderer,
|
||||
backgroundColor: Color(0x000000)
|
||||
}
|
||||
});
|
||||
|
||||
// If the snapshot provides a target, adjust the canvas `sceneRadiusFactor` so the scene isn't clipped
|
||||
// when we switch camera.
|
||||
if (snapshot.target) {
|
||||
try {
|
||||
const boundingSphere = getPluginBoundingSphere(plugin);
|
||||
if (boundingSphere && boundingSphere.radius > 0) {
|
||||
const offset = Vec3.distance(snapshot.target as Vec3, boundingSphere.center);
|
||||
const sceneRadiusFactor = (boundingSphere.radius + offset) / boundingSphere.radius;
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback: ignore errors and continue to set the camera snapshot
|
||||
console.warn('Failed to adjust sceneRadiusFactor for view snapshot', e);
|
||||
}
|
||||
}
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to parse Kinemage data from string/data input
|
||||
*/
|
||||
export const ParseKinemage = Transform({
|
||||
name: 'sb-kinemage-parse',
|
||||
display: { name: 'Parse Kinemage' },
|
||||
from: [PluginStateObject.Data.String],
|
||||
to: KinemageObject,
|
||||
params: {
|
||||
label: PD.Optional(PD.Text('', { description: 'Label for the Kinemage data' }))
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Parse Kinemage', async ctx => {
|
||||
const input = a.data;
|
||||
let data: KinemageData;
|
||||
|
||||
if (typeof input === 'string') {
|
||||
// Parse from string content
|
||||
const file = new File([input], 'input.kin', { type: 'text/plain' });
|
||||
data = await KinemageData.open(file);
|
||||
} else {
|
||||
throw new Error('Unsupported input type for ParseKinemage');
|
||||
}
|
||||
|
||||
// Precompute camera snapshots for all views in all kinemages
|
||||
for (const kinData of data.kinemages) {
|
||||
(kinData as any).viewSnapshots = (kinData as any).viewSnapshots || Object.create(null);
|
||||
for (const [viewKey, viewObj] of Object.entries(kinData.viewDict)) {
|
||||
const center = Vec3.create(0, 0, 0);
|
||||
if (viewObj.center) {
|
||||
Vec3.set(center, viewObj.center[0], viewObj.center[1], viewObj.center[2]);
|
||||
}
|
||||
|
||||
const orientation: Mat3 = Mat3.identity();
|
||||
if (viewObj.matrix) {
|
||||
Mat3.fromArray(orientation, viewObj.matrix, 0);
|
||||
Mat3.transpose(orientation, orientation);
|
||||
}
|
||||
|
||||
const zAxis = Vec3.create(0, 0, 1);
|
||||
Vec3.transformMat3(zAxis, zAxis, orientation);
|
||||
|
||||
const yAxis = Vec3.create(0, 1, 0);
|
||||
Vec3.transformMat3(yAxis, yAxis, orientation);
|
||||
|
||||
let distance = 100;
|
||||
if (viewObj.span) {
|
||||
distance = viewObj.span;
|
||||
}
|
||||
Vec3.scale(zAxis, zAxis, distance);
|
||||
const position = Vec3.create(0, 0, 100);
|
||||
Vec3.add(position, center, zAxis);
|
||||
|
||||
let radius = 100;
|
||||
if (viewObj.zslab) {
|
||||
const scale = viewObj.zslab / 200;
|
||||
radius = 0.5 * distance * scale;
|
||||
}
|
||||
|
||||
const snap: Camera.Snapshot = {
|
||||
mode: 'orthographic',
|
||||
fov: Math.PI / 4,
|
||||
position,
|
||||
up: yAxis,
|
||||
target: center,
|
||||
radius,
|
||||
radiusMax: 1e4,
|
||||
fog: 0,
|
||||
clipFar: true,
|
||||
minNear: 1,
|
||||
minFar: 1
|
||||
};
|
||||
|
||||
(kinData as any).viewSnapshots[viewKey] = snap;
|
||||
}
|
||||
}
|
||||
|
||||
const label = params.label || data.kinemages[0]?.caption || 'Kinemage';
|
||||
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} view(s)` });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Transform to select a specific kinemage from parsed data
|
||||
*/
|
||||
export const SelectKinemage = Transform({
|
||||
name: 'sb-kinemage-select',
|
||||
display: { name: 'Select Kinemage' },
|
||||
from: KinemageObject,
|
||||
to: PluginStateObject.Format.Json,
|
||||
params: (a) => {
|
||||
const kinemages = a?.data?.kinemages || [];
|
||||
const options = kinemages.map((k: Kinemage, i: number) => [i, k.pdbfile || k.caption || `Kinemage ${i}`] as const);
|
||||
return {
|
||||
index: PD.Select(0, options, { description: 'Which kinemage to use' })
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Select Kinemage', async ctx => {
|
||||
const kinData = a.data.kinemages[params.index];
|
||||
if (!kinData) {
|
||||
throw new Error(`No kinemage found at index ${params.index}`);
|
||||
}
|
||||
|
||||
const label = kinData.pdbfile || kinData.caption || `Kinemage ${params.index}`;
|
||||
|
||||
// Store the kinemage data in a Format.Json node so downstream transforms can access it
|
||||
return new PluginStateObject.Format.Json(
|
||||
{ kinData },
|
||||
{ label, description: kinData.text || '' }
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapePointsProvider = Transform({
|
||||
name: 'sb-kinemage-shape-points-provider',
|
||||
display: { name: 'Kinemage Shape Points Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Points Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapePointsFromKin(kinData, { transforms: undefined }, 'Dots').runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Points',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeLinesProvider = Transform({
|
||||
name: 'sb-kinemage-shape-lines-provider',
|
||||
display: { name: 'Kinemage Shape Lines Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Lines Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeLinesFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Lines',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeMeshProvider = Transform({
|
||||
name: 'sb-kinemage-shape-mesh-provider',
|
||||
display: { name: 'Kinemage Shape Mesh Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Mesh Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeMeshFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Meshes',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeSpheresProvider = Transform({
|
||||
name: 'sb-kinemage-shape-spheres-provider',
|
||||
display: { name: 'Kinemage Shape Spheres Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Spheres Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeSpheresFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Spheres',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'kinemage-data-prop',
|
||||
category: 'custom-props',
|
||||
display: {
|
||||
name: 'Kinemage data',
|
||||
description: 'Data loaded from Kinemage.'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
private provider = KinemageDataProvider;
|
||||
|
||||
register(): void {
|
||||
DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
|
||||
|
||||
this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
|
||||
|
||||
// Register right-panel controls for Kinemage (show in the right-hand inspector)
|
||||
this.ctx.customStructureControls.set(Tag.Representation, KinemageControls as any);
|
||||
// Some app hosts expose a global customControls registry; register there too so the card is visible
|
||||
// even when no structure is loaded. Use `any` guards to avoid type errors if customControls isn't present.
|
||||
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.set === 'function') {
|
||||
(this.ctx as any).customControls.set('kinemage', KinemageControls as any);
|
||||
}
|
||||
|
||||
this.ctx.managers.dragAndDrop.addHandler(KinemageDragAndDropHandler.name, KinemageDragAndDropHandler.handle);
|
||||
|
||||
// Register .kin file handler so opening/dropping .kin is supported via the data formats system
|
||||
this.ctx.dataFormats.add('KIN', KINFormatProvider);
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean }) {
|
||||
const 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.genericRepresentationControls.delete(Tag.Representation);
|
||||
|
||||
this.ctx.managers.dragAndDrop.removeHandler(KinemageDragAndDropHandler.name);
|
||||
|
||||
// Unregister the .kin data format provider
|
||||
this.ctx.dataFormats.remove('KIN');
|
||||
|
||||
// Remove right-panel controls
|
||||
try { this.ctx.customStructureControls.delete(Tag.Representation); } catch { }
|
||||
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.delete === 'function') {
|
||||
try { (this.ctx as any).customControls.delete('kinemage'); } catch { }
|
||||
}
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(false)
|
||||
})
|
||||
});
|
||||
|
||||
/** Registerable method for handling dragged-and-dropped files */
|
||||
interface DragAndDropHandler {
|
||||
name: string,
|
||||
handle: PluginDragAndDropHandler,
|
||||
}
|
||||
|
||||
/** Helper function to create all shapes for a kinemage via proper transform chain */
|
||||
async function createShapesForKinemage(plugin: PluginContext, update: StateBuilder.Root, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
const kinDataCell = plugin.state.data.cells.get(kinDataSelector.ref);
|
||||
if (!kinDataCell?.obj?.data) return;
|
||||
|
||||
const kinData = (kinDataCell.obj.data as any).kinData as Kinemage;
|
||||
if (!kinData) return;
|
||||
|
||||
// Generate all shape types that have data, each as child of the selected kinemage
|
||||
if (kinData.dotLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.vectorLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.ribbonLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
|
||||
}
|
||||
if (kinData.ballLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to rebuild shapes for a kinemage (remove and recreate) */
|
||||
export async function rebuildShapesForKinemage(plugin: PluginContext, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
// Store current camera snapshot
|
||||
const curSnap = (plugin.canvas3d && (plugin.canvas3d as any).camera && (plugin.canvas3d as any).camera.getSnapshot)
|
||||
? (plugin.canvas3d as any).camera.getSnapshot()
|
||||
: undefined;
|
||||
|
||||
const update = plugin.state.data.build();
|
||||
|
||||
// Remove all children of this kinemage node (shapes/representations)
|
||||
const children = plugin.state.data.tree.children.get(kinDataSelector.ref);
|
||||
if (children) {
|
||||
for (const childRef of children.values()) {
|
||||
update.delete(childRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate shapes
|
||||
await createShapesForKinemage(plugin, update, kinDataSelector);
|
||||
await update.commit();
|
||||
|
||||
// Restore camera
|
||||
if (curSnap) {
|
||||
try {
|
||||
await applyViewSnapshot(plugin, curSnap);
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore camera snapshot after recreating shapes', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Centralized helper to apply kinemage content into plugin state */
|
||||
async function applyKinemageToState(plugin: PluginContext, data: string, label?: string) {
|
||||
const update = plugin.state.data.build();
|
||||
|
||||
// Create String data node
|
||||
const dataNode = update
|
||||
.toRoot()
|
||||
.apply(StateTransforms.Data.RawData, { data, label: label || 'Kinemage File' });
|
||||
|
||||
// Parse into KinemageObject
|
||||
const parsedNode = dataNode
|
||||
.apply(ParseKinemage, { label });
|
||||
|
||||
// Select first kinemage (default)
|
||||
const selectedNode = parsedNode
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Now create shapes from the selected kinemage
|
||||
const shapeUpdate = plugin.state.data.build();
|
||||
await createShapesForKinemage(plugin, shapeUpdate, selectedNode.selector);
|
||||
await shapeUpdate.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const bs = getPluginBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0) return bs;
|
||||
} catch { /* ignore */ }
|
||||
await new Promise<void>(r => setTimeout(r, pollMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const bs = await waitForNonEmptyBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0 && plugin.canvas3d) {
|
||||
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
|
||||
plugin.canvas3d?.commit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to apply initial kinemage view snapshot', e);
|
||||
}
|
||||
|
||||
return selectedNode.selector;
|
||||
}
|
||||
|
||||
/** Programmatic loader: load a single File (a .kin) into the plugin state.
|
||||
* Returns the ref to the selected kinemage node.
|
||||
*/
|
||||
export async function loadKinemageFile(plugin: PluginContext, file: File): Promise<StateObjectSelector<PluginStateObject.Format.Json> | undefined> {
|
||||
const content = await file.text();
|
||||
return await applyKinemageToState(plugin, content, file.name);
|
||||
}
|
||||
|
||||
/** DragAndDropHandler handler for `.kin` files */
|
||||
const KinemageDragAndDropHandler: DragAndDropHandler = {
|
||||
name: 'kin',
|
||||
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
|
||||
let applied = false;
|
||||
for (const file of files) {
|
||||
if (file.name.toLowerCase().endsWith('.kin')) {
|
||||
const ref = await loadKinemageFile(plugin, file);
|
||||
applied = applied || !!ref;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
},
|
||||
};
|
||||
|
||||
const KINFormatProvider: DataFormatProvider<{}, any, any> = DataFormatProvider({
|
||||
label: 'KIN',
|
||||
description: 'Kinemage',
|
||||
category: 'Miscellaneous',
|
||||
stringExtensions: ['kin', 'KIN'],
|
||||
parse: async (plugin, data) => {
|
||||
try {
|
||||
// data is already a StateObjectRef to the raw data in the tree
|
||||
// Build the transform chain from it
|
||||
const builder = plugin.state.data.build()
|
||||
.to(data)
|
||||
.apply(ParseKinemage, {});
|
||||
|
||||
const selectedKin = builder
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await builder.commit();
|
||||
|
||||
// Return the selector for the selected kinemage so visuals can use it
|
||||
return { selectedKin: selectedKin.selector };
|
||||
} catch (e) {
|
||||
console.error('Failed to parse KIN file', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
visuals: async (plugin, data) => {
|
||||
if (!data?.selectedKin) {
|
||||
console.warn('[Kinemage] visuals: no selectedKin ref provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create shapes from the selected kinemage
|
||||
const shapeBuilder = plugin.state.data.build();
|
||||
await createShapesForKinemage(plugin, shapeBuilder, data.selectedKin);
|
||||
await shapeBuilder.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const bs = getPluginBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0) return bs;
|
||||
} catch { /* ignore */ }
|
||||
await new Promise<void>(r => setTimeout(r, pollMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const bs = await waitForNonEmptyBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0 && plugin.canvas3d) {
|
||||
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
|
||||
plugin.canvas3d?.commit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to focus camera on kinemage', e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
584
src/extensions/kinemage/kin.ts
Normal file
584
src/extensions/kinemage/kin.ts
Normal file
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { ShapeProvider } from '../../mol-model/shape/provider';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Kinemage, DotList, VectorList, RibbonObject, BallList } from './reader/schema';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Points } from '../../mol-geo/geometry/points/points';
|
||||
import { PointsBuilder } from '../../mol-geo/geometry/points/points-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
|
||||
import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
export type KinData = {
|
||||
source: Kinemage,
|
||||
transforms?: Mat4[],
|
||||
}
|
||||
|
||||
function createKinShapePointsParams(kinemage?: Kinemage) {
|
||||
return {
|
||||
...Points.Params,
|
||||
};
|
||||
}
|
||||
export const KinShapePointsParams = createKinShapePointsParams();
|
||||
export type KinShapePointsParams = typeof KinShapePointsParams
|
||||
function createKinShapeLinesParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Lines.Params,
|
||||
};
|
||||
}
|
||||
export const KinShapeLinesParams = createKinShapeLinesParams();
|
||||
export type KinShapeLinesParams = typeof KinShapeLinesParams
|
||||
function createKinShapeMeshParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Mesh.Params,
|
||||
// transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const)),
|
||||
// doubleSided: PD.Boolean(true), // make mesh double-sided by default
|
||||
// ignoreLight: PD.Boolean(true), // ignore lighting so front/back show same color
|
||||
};
|
||||
}
|
||||
|
||||
export const KinShapeMeshParams = createKinShapeMeshParams();
|
||||
export type KinShapeMeshParams = typeof KinShapeMeshParams
|
||||
|
||||
function createKinShapeSpheresParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Spheres.Params,
|
||||
};
|
||||
}
|
||||
|
||||
export const KinShapeSpheresParams = createKinShapeSpheresParams();
|
||||
export type KinShapeSpheresParams = typeof KinShapeSpheresParams;
|
||||
|
||||
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage) {
|
||||
let visible = true;
|
||||
|
||||
// Check to see if this name references a master that is not visible. If so, then this whole list is not visible and we can skip it.
|
||||
const masterDict = kin.masterDict;
|
||||
for (let m = 0; m < masters.length; m++) {
|
||||
const masterName = masters[m];
|
||||
const masterInfo = masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if this name references a group that has the 'off' flag set. If so, this is not visible.
|
||||
const groupDict = kin.groupDict;
|
||||
const groupInfo = groupDict[group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Check to see if this name references a subgroup that it or its master has the 'off' flag set. If so, this is not visible.
|
||||
const subgroupDict = kin.subgroupDict;
|
||||
const subgroupInfo = subgroupDict[subGroup];
|
||||
if (subgroupInfo) {
|
||||
if (subgroupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
if (subgroupInfo.group) {
|
||||
const groupInfo = groupDict[subgroupInfo.group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function getPoints(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const dotLists: DotList[] = kin.dotLists;
|
||||
const builderState = PointsBuilder.create();
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every dot is in its own Molstar group because they may have colors and we look that up by group.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < dotLists.length; i++) {
|
||||
const dotList = dotLists[i];
|
||||
const positionArray = dotList.positionArray;
|
||||
const colorArray = dotList.colorArray;
|
||||
const labelArray = dotList.labelArray;
|
||||
const masterArray = dotList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this dot list if any of them are not visible.
|
||||
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numDots = positionArray.length / 3;
|
||||
for (let j = 0; j < numDots; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = dotList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const group = index++;
|
||||
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(labelArray && labelArray.length > j ? labelArray[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const points = builderState.getPoints();
|
||||
return { points, colors, labels };
|
||||
}
|
||||
|
||||
async function getLines(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const vectorLists: VectorList[] = kin.vectorLists;
|
||||
const builderState = LinesBuilder.create();
|
||||
const widths: number[] = [];
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every line is in its own Molstar group because they may have individual widths and we look
|
||||
// up the width based on the group is in the size function.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < vectorLists.length; i++) {
|
||||
const vectorList = vectorLists[i];
|
||||
const position1Array = vectorList.position1Array;
|
||||
const position2Array = vectorList.position2Array;
|
||||
const widthArray = vectorList.width;
|
||||
const color1Array = vectorList.color1Array;
|
||||
const color2Array = vectorList.color2Array;
|
||||
const label1Array = vectorList.label1Array;
|
||||
const label2Array = vectorList.label2Array;
|
||||
const masterArray = vectorList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this vector list if any of them are not visible.
|
||||
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numLines = position1Array.length / 3;
|
||||
for (let j = 0; j < numLines; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = vectorList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
// Find the midpoint of the line because we're going to actually make
|
||||
// two half-lines so that labels and selection work better.
|
||||
const midX = (position1Array[3 * j + 0] + position2Array[3 * j + 0]) / 2;
|
||||
const midY = (position1Array[3 * j + 1] + position2Array[3 * j + 1]) / 2;
|
||||
const midZ = (position1Array[3 * j + 2] + position2Array[3 * j + 2]) / 2;
|
||||
|
||||
// Make the first half of the line from position1 to the midpoint, labeled and colored based on position1.
|
||||
let group = index++;
|
||||
builderState.add(position1Array[3 * j + 0], position1Array[3 * j + 1], position1Array[3 * j + 2],
|
||||
midX, midY, midZ,
|
||||
group);
|
||||
// widthArray may be undefined; push NaN when width not provided
|
||||
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(color1Array && color1Array.length > j ? color1Array[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(label1Array && label1Array.length > j ? label1Array[j] : '');
|
||||
|
||||
// Make the second half of the line from the midpoint to position2, labeled and colored based on position2.
|
||||
group = index++;
|
||||
builderState.add(midX, midY, midZ,
|
||||
position2Array[3 * j + 0], position2Array[3 * j + 1], position2Array[3 * j + 2],
|
||||
group);
|
||||
// widthArray may be undefined; push NaN when width not provided
|
||||
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(color2Array && color2Array.length > j ? color2Array[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(label2Array && label2Array.length > j ? label2Array[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const lines = builderState.getLines();
|
||||
return { lines, widths: new Float32Array(widths), colors, labels };
|
||||
}
|
||||
|
||||
function addOffsetTriangle(builderState: MeshBuilder.State, a: Vec3, b: Vec3, c: Vec3, n: Vec3, offset: number) {
|
||||
const aOffset = Vec3.add(Vec3(), a, Vec3.scale(Vec3(), n, offset));
|
||||
const bOffset = Vec3.add(Vec3(), b, Vec3.scale(Vec3(), n, offset));
|
||||
const cOffset = Vec3.add(Vec3(), c, Vec3.scale(Vec3(), n, offset));
|
||||
MeshBuilder.addTriangleWithNormal(builderState, aOffset, bOffset, cOffset, n);
|
||||
}
|
||||
|
||||
async function getMesh(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const ribbonObjects: RibbonObject[] = kin.ribbonLists;
|
||||
const builderState = MeshBuilder.createState();
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every triangle is in its own Molstar group because they may have individual colors and we look
|
||||
// up the color based on the group is in the color function.
|
||||
let index = 0;
|
||||
|
||||
for (let ri = 0; ri < ribbonObjects.length; ri++) {
|
||||
const ribbonObject = ribbonObjects[ri];
|
||||
const coords = ribbonObject.positionArray;
|
||||
const colorArray = ribbonObject.colorArray;
|
||||
const labelArray = ribbonObject.labelArray;
|
||||
const masterArray = ribbonObject.masterArray;
|
||||
const pointMasterArray = ribbonObject.pointmasterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ribbon object if any of them are not visible.
|
||||
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
builderState.currentGroup = ri; // TODO: Base this on something in the file instead?
|
||||
|
||||
// The positionArray contains 3x as many entries as there are vertices since it's a catenation of x, y, z for each vertex.
|
||||
// There are three vertices per triangle.
|
||||
// TODO: Ribbon lighting is to be set up to make each pair of triangles look like a quad with the same normal.
|
||||
const numTriangles = coords.length / 9;
|
||||
let prevTriangleNormal: Vec3 | undefined = undefined;
|
||||
for (let i = 0; i < numTriangles; i++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = pointMasterArray[3 * i];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const vertexList: Vec3[] = [];
|
||||
|
||||
// Get the vertices for the triangle out of the position array and push them onto a list.
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const v = Vec3.zero();
|
||||
v[0] = coords[3 * (3 * i + j) + 0];
|
||||
v[1] = coords[3 * (3 * i + j) + 1];
|
||||
v[2] = coords[3 * (3 * i + j) + 2];
|
||||
vertexList.push(v);
|
||||
}
|
||||
|
||||
// Set the group per triangle so that we can do per-triangle coloring.
|
||||
const group = index++;
|
||||
builderState.currentGroup = group;
|
||||
|
||||
// colorArray may be undefined; push a default color when not provided.
|
||||
// There is one color per group, even if we have two triangles in this group.
|
||||
const color = colorArray && colorArray.length > i * 3 ? colorArray[3 * i] : Color.fromRgb(255, 255, 255);
|
||||
colors.push(color);
|
||||
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
const label = labelArray && labelArray.length > i ? labelArray[i] : '';
|
||||
labels.push(label);
|
||||
|
||||
// Find the vertics and normal for the triangle.
|
||||
const a: Vec3 = vertexList[0];
|
||||
const b: Vec3 = vertexList[1];
|
||||
const c: Vec3 = vertexList[2];
|
||||
|
||||
// Put both orientations of the triangle. Add a small amount along the normal to make them
|
||||
// not be exactly on top of each other so that we only see the front face of each.
|
||||
let n = Vec3.zero();
|
||||
Vec3.triangleNormal(n, a, b, c);
|
||||
if (i % 2 === 1) {
|
||||
// For ribbons, every other triangle is meant to be paired with the previous one to make a quad with the same normal.
|
||||
// So use the same normal for every other triangle.
|
||||
n = prevTriangleNormal || n;
|
||||
}
|
||||
prevTriangleNormal = n;
|
||||
addOffsetTriangle(builderState, a, b, c, n, 0.01);
|
||||
|
||||
// Invert the normal for the back face.
|
||||
Vec3.negate(n, n);
|
||||
addOffsetTriangle(builderState, a, c, b, n, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(builderState);
|
||||
return { mesh, colors, labels };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build spheres geometry and collect per-sphere radii from the KIN BallList entries.
|
||||
* Returns an object with the Spheres geometry and a Float32Array with per-center radii (one entry per center, in the same order they were added).
|
||||
*/
|
||||
async function getSpheres(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const balls: BallList[] = kin.ballLists;
|
||||
const builderState = SpheresBuilder.create();
|
||||
const radii: number[] = [];
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every ball is in its own Molstar group because they may have individual radii and we look
|
||||
// up the radius based on the group is in the size function.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < balls.length; i++) {
|
||||
const ballList = balls[i];
|
||||
const positionArray = ballList.positionArray;
|
||||
const radiusArray = ballList.radiusArray;
|
||||
const colorArray = ballList.colorArray;
|
||||
const masterArray = ballList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ball list if any of them are not visible.
|
||||
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numBalls = positionArray.length / 3;
|
||||
for (let j = 0; j < numBalls; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = ballList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const group = index++;
|
||||
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
|
||||
// radiusArray may be undefined; push NaN when radius not provided
|
||||
radii.push(radiusArray && radiusArray.length > j ? radiusArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(ballList.labelArray && ballList.labelArray.length > j ? ballList.labelArray[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const spheres = builderState.getSpheres();
|
||||
return { spheres, radii: new Float32Array(radii), colors, labels };
|
||||
}
|
||||
|
||||
function makePointsShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapePointsParams>, shape?: Shape<Points>) => {
|
||||
// Get our points, adding them from all of the entries in the dot lists
|
||||
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source);
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Points>(
|
||||
'kin-points',
|
||||
kinData.source,
|
||||
_points,
|
||||
colorFn, // color function reads per-point colors
|
||||
() => 1, // size function
|
||||
labelFn // label function reads per-point labels
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeLineShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeLinesParams>, shape?: Shape<Lines>) => {
|
||||
// Get our lines, adding them from all of the entries in the vector lists
|
||||
const { lines: _lines, widths, colors, labels } = await getLines(ctx, kinData.source);
|
||||
|
||||
// Size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const sizeFn = (group: number, instance: number) => {
|
||||
// We're specifying the radius, which is half the width.
|
||||
let w = widths[group] / 2.0;
|
||||
if (w < 1.0) { w = 1.0; }
|
||||
return Number.isFinite(w) ? w : 1.0;
|
||||
};
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Lines>(
|
||||
'kin-lines',
|
||||
kinData.source,
|
||||
_lines,
|
||||
colorFn, // color function reads per-line colors
|
||||
sizeFn, // size function reads per-line widths
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeMeshShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeMeshParams>, shape?: Shape<Mesh>) => {
|
||||
|
||||
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source);
|
||||
// Ensure that _mesh is not undifined before we pass it to Shape.create. If it is undefined, create an empty mesh instead.
|
||||
if (!_mesh) {
|
||||
console.warn('No mesh could be created from the KIN data. Creating an empty mesh instead.');
|
||||
_mesh = Mesh.createEmpty();
|
||||
}
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Mesh>(
|
||||
'kin-mesh',
|
||||
kinData.source,
|
||||
_mesh,
|
||||
colorFn, // color function reads per-triangle colors
|
||||
() => 1, // size function
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spheres shape getter: uses per-center radii read from the KIN BallList radiusArray when available.
|
||||
*/
|
||||
function makeSpheresShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeSpheresParams>, shape?: Shape<Spheres>) => {
|
||||
// Build spheres geometry and collect per-center radii
|
||||
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source);
|
||||
|
||||
// size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const sizeFn = (group: number, instance: number) => {
|
||||
const r = radii[group];
|
||||
return Number.isFinite(r) ? r : 1.0;
|
||||
};
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Spheres>(
|
||||
'kin-spheres',
|
||||
kinData.source,
|
||||
_spheres,
|
||||
colorFn, // color function reads per-center colors
|
||||
sizeFn, // size function reads per-center radii
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
export function shapePointsFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Points, KinShapePointsParams>>('Kin Shape Points Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Points',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapePointsParams(source),
|
||||
getShape: makePointsShapeGetter(),
|
||||
geometryUtils: Points.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeLinesFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Lines, KinShapeLinesParams>>('Kin Shape Lines Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Lines',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeLinesParams(source),
|
||||
getShape: makeLineShapeGetter(),
|
||||
geometryUtils: Lines.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeMeshFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Mesh, KinShapeMeshParams>>('Kin Shape Mesh Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Meshes',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeMeshParams(source),
|
||||
getShape: makeMeshShapeGetter(),
|
||||
geometryUtils: Mesh.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeSpheresFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Spheres, KinShapeSpheresParams>>('Kin Shape Spheres Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Spheres',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeSpheresParams(source),
|
||||
getShape: makeSpheresShapeGetter(),
|
||||
geometryUtils: Spheres.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
116
src/extensions/kinemage/prop.ts
Normal file
116
src/extensions/kinemage/prop.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
import { Kinemage } from './reader/schema';
|
||||
import { parseKin } from './reader/parser';
|
||||
|
||||
export const KinemageParams = {
|
||||
};
|
||||
export type KinemageParams = typeof KinemageParams
|
||||
export type KinemageProps = PD.Values<KinemageParams>
|
||||
|
||||
export const KinemageDataParams = {
|
||||
...KinemageParams
|
||||
};
|
||||
export type KinemageDataParams = typeof KinemageDataParams
|
||||
export type KinemageDataProps = PD.Values<KinemageDataParams>
|
||||
|
||||
export { KinemageData };
|
||||
|
||||
interface KinemageData {
|
||||
/**
|
||||
* List of Kinemages read from one or more files.
|
||||
*/
|
||||
readonly kinemages: Kinemage[]
|
||||
}
|
||||
|
||||
const FileSourceParams = {
|
||||
input: PD.File({ accept: '.kin', multiple: false })
|
||||
};
|
||||
type FileSourceProps = PD.Values<typeof FileSourceParams>
|
||||
|
||||
namespace KinemageData {
|
||||
export enum Tag {
|
||||
Representation = 'kinemage-3d'
|
||||
}
|
||||
|
||||
export const symbols = {
|
||||
};
|
||||
|
||||
async function loadKinemageData(data: string): Promise<Kinemage[]> {
|
||||
const task = parseKin(data);
|
||||
const result = await task.run();
|
||||
if (result.isError) {
|
||||
throw new Error('Failed to parse KIN data');
|
||||
}
|
||||
return result.result;
|
||||
}
|
||||
|
||||
export async function open(file: FileSourceProps | File): Promise<KinemageData> {
|
||||
|
||||
let fileToRead: File;
|
||||
|
||||
if (file instanceof File) {
|
||||
fileToRead = file;
|
||||
} else if (file && file.input && file.input.file) {
|
||||
fileToRead = file.input.file;
|
||||
} else {
|
||||
throw new Error('No file given');
|
||||
}
|
||||
|
||||
const task = Task.create('Load KIN file', async ctx => {
|
||||
const data = await fileToRead.text();
|
||||
const kinemages = await loadKinemageData(data);
|
||||
return kinemages;
|
||||
});
|
||||
|
||||
const kinemages = await task.run();
|
||||
return { kinemages };
|
||||
}
|
||||
}
|
||||
|
||||
export const KinemageDataProvider: CustomStructureProperty.Provider<KinemageDataParams, KinemageData> = CustomStructureProperty.createProvider({
|
||||
label: 'Kinemage',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'Kinemage_loaded_data',
|
||||
symbols: KinemageData.symbols,
|
||||
}),
|
||||
type: 'root',
|
||||
defaultParams: KinemageDataParams,
|
||||
getParams: (data: Structure) => KinemageDataParams,
|
||||
isApplicable,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<KinemageDataProps>) => {
|
||||
const p = { ...PD.getDefaultValues(KinemageDataParams), ...props };
|
||||
try {
|
||||
return { value: await computeKinemageProps(ctx, data, p) };
|
||||
} catch (e) {
|
||||
// the "Residues Embedded in Membrane" symbol may bypass isApplicable() checks
|
||||
console.warn('Failed to predict membrane orientation. This happens for short peptides and entries without amino acids.');
|
||||
return { value: undefined };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function isApplicable(structure: Structure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function computeKinemageProps(ctx: CustomProperty.Context, data: Structure, props: Partial<KinemageProps>): Promise<KinemageData> {
|
||||
// Return an empty KinemageData object since the actual data will be loaded asynchronously via the `open` method.
|
||||
// This allows the property to be attached to the structure without blocking on file loading.
|
||||
return {
|
||||
kinemages: []
|
||||
};
|
||||
}
|
||||
944
src/extensions/kinemage/reader/kinparser.ts
Normal file
944
src/extensions/kinemage/reader/kinparser.ts
Normal file
@@ -0,0 +1,944 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Based on earlier kin-parser.ts file from the NGL project (see second author notice below).
|
||||
* @file Ported NGL-based Kinemage file parser
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* file Kin Parser
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Kinemage, RibbonObject } from './schema';
|
||||
import { Hsv } from '../../../mol-util/color/spaces/hsv';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
const ColorDict: { [k: string]: Color } = {
|
||||
red: Hsv.toColor(Hsv.fromArray([0, 100, 100])),
|
||||
orange: Hsv.toColor(Hsv.fromArray([20, 100, 100])),
|
||||
gold: Hsv.toColor(Hsv.fromArray([40, 100, 100])),
|
||||
yellow: Hsv.toColor(Hsv.fromArray([60, 100, 100])),
|
||||
lime: Hsv.toColor(Hsv.fromArray([80, 100, 100])),
|
||||
green: Hsv.toColor(Hsv.fromArray([120, 80, 100])),
|
||||
sea: Hsv.toColor(Hsv.fromArray([150, 100, 100])),
|
||||
cyan: Hsv.toColor(Hsv.fromArray([180, 100, 85])),
|
||||
sky: Hsv.toColor(Hsv.fromArray([210, 75, 95])),
|
||||
blue: Hsv.toColor(Hsv.fromArray([240, 70, 100])),
|
||||
purple: Hsv.toColor(Hsv.fromArray([275, 75, 100])),
|
||||
magenta: Hsv.toColor(Hsv.fromArray([300, 95, 100])),
|
||||
hotpink: Hsv.toColor(Hsv.fromArray([335, 100, 100])),
|
||||
pink: Hsv.toColor(Hsv.fromArray([350, 55, 100])),
|
||||
peach: Hsv.toColor(Hsv.fromArray([25, 75, 100])),
|
||||
lilac: Hsv.toColor(Hsv.fromArray([275, 55, 100])),
|
||||
pinktint: Hsv.toColor(Hsv.fromArray([340, 30, 100])),
|
||||
peachtint: Hsv.toColor(Hsv.fromArray([25, 50, 100])),
|
||||
yellowtint: Hsv.toColor(Hsv.fromArray([60, 50, 100])),
|
||||
greentint: Hsv.toColor(Hsv.fromArray([135, 40, 100])),
|
||||
bluetint: Hsv.toColor(Hsv.fromArray([220, 40, 100])),
|
||||
lilactint: Hsv.toColor(Hsv.fromArray([275, 35, 100])),
|
||||
white: Hsv.toColor(Hsv.fromArray([0, 0, 100])),
|
||||
gray: Hsv.toColor(Hsv.fromArray([0, 0, 50])),
|
||||
brown: Hsv.toColor(Hsv.fromArray([20, 45, 75])),
|
||||
deadwhite: Hsv.toColor(Hsv.fromArray([0, 0, 100])),
|
||||
deadblack: Hsv.toColor(Hsv.fromArray([0, 0, 0])),
|
||||
invisible: Hsv.toColor(Hsv.fromArray([0, 0, 0]))
|
||||
};
|
||||
|
||||
const reWhitespaceComma = /[\s,]+/;
|
||||
const reCurlyWhitespace = /[^{}\s]*{[^{}]+}|[^{}\s]+/g;
|
||||
const reTrimCurly = /^{+|}+$/g;
|
||||
const reCollapseEqual = /\s*=\s*/g;
|
||||
|
||||
function parseListDef(line: string, localColorDict: { [k: string]: Color }) {
|
||||
let name;
|
||||
let defaultColor: Color = localColorDict['white']; // Default color is white, but it can be overridden by the list definition
|
||||
let radius;
|
||||
let nobutton = false;
|
||||
const master = [];
|
||||
let width = 2; // Default width is 2, but it can be overridden by the list definition
|
||||
|
||||
line = line.replace(reCollapseEqual, '=');
|
||||
|
||||
const lm = line.match(reCurlyWhitespace) as RegExpMatchArray;
|
||||
for (let j = 1; j < lm.length; ++j) {
|
||||
const e = lm[j];
|
||||
if (e[0] === '{') {
|
||||
name = e.substring(1, e.length - 1);
|
||||
} else {
|
||||
const es = e.split('=');
|
||||
if (es.length === 2) {
|
||||
if (es[0] === 'color') {
|
||||
const colorName = parseStr(es[1]);
|
||||
defaultColor = localColorDict[colorName];
|
||||
} else if (es[0] === 'width') {
|
||||
width = parseInt(es[1]);
|
||||
} else if (es[0] === 'master') {
|
||||
master.push(es[1].replace(reTrimCurly, ''));
|
||||
} else if (es[0] === 'radius') {
|
||||
radius = parseFloat(es[1]);
|
||||
} else {
|
||||
console.log('Kinemage: Unknown list definition term found: ' + es[0]);
|
||||
}
|
||||
} else if (e === 'nobutton') {
|
||||
nobutton = true;
|
||||
} else {
|
||||
console.log('Kinemage: Unknown list definition term found: ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
listName: name,
|
||||
listColor: defaultColor,
|
||||
listMasters: master,
|
||||
listWidth: width,
|
||||
listRadius: radius,
|
||||
nobutton: nobutton
|
||||
};
|
||||
}
|
||||
|
||||
function parseListElm(line: string, localColorDict: { [k: string]: Color }) {
|
||||
line = line.trim();
|
||||
|
||||
const idx1 = line.indexOf('{');
|
||||
const idx2 = line.indexOf('}');
|
||||
const ls = line.substr(idx2 + 1).split(reWhitespaceComma);
|
||||
|
||||
const label = line.substr(idx1 + 1, idx2 - 1);
|
||||
const position = [
|
||||
parseFloat(ls[ls.length - 3]),
|
||||
parseFloat(ls[ls.length - 2]),
|
||||
parseFloat(ls[ls.length - 1])
|
||||
];
|
||||
let color, width, radius;
|
||||
let lineBreak = false;
|
||||
let triangleBreak = false;
|
||||
const pointMasters: string[] = [];
|
||||
for (let lsindex = 4; lsindex <= ls.length; lsindex++) {
|
||||
const literal = ls[ls.length - lsindex];
|
||||
if (literal in localColorDict) {
|
||||
color = localColorDict[ls[ls.length - lsindex]];
|
||||
}
|
||||
if (literal.startsWith('width')) {
|
||||
width = parseInt(literal.substring(5));
|
||||
}
|
||||
if (literal.startsWith('r=')) {
|
||||
radius = parseFloat(literal.split('=')[1]);
|
||||
}
|
||||
if (literal.startsWith('P')) {
|
||||
lineBreak = true;
|
||||
}
|
||||
if (literal.startsWith('X')) {
|
||||
triangleBreak = true;
|
||||
}
|
||||
if (literal.startsWith("'") && literal.endsWith("'")) {
|
||||
// Handle single-character tags by putting each character into a pointMaster tag, e.g. 'ab' would be two tags, 'a' and 'b'
|
||||
const tagString: string = literal.substring(1, literal.length - 1);
|
||||
for (let i = 0; i < tagString.length; i++) {
|
||||
pointMasters.push(tagString[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: label,
|
||||
position: position,
|
||||
color: color,
|
||||
radius: radius,
|
||||
width: width,
|
||||
isLineBreak: lineBreak,
|
||||
isTriangleBreak: triangleBreak,
|
||||
pointMasters: pointMasters
|
||||
};
|
||||
}
|
||||
|
||||
function parseStr(line: string) {
|
||||
const start = line.indexOf('{');
|
||||
const end = line.indexOf('}');
|
||||
return line.substring(
|
||||
start !== -1 ? start + 1 : 0,
|
||||
end !== -1 ? end : undefined
|
||||
).trim();
|
||||
}
|
||||
|
||||
function parseFlag(line: string) {
|
||||
const end = line.indexOf('}');
|
||||
return end === -1 ? undefined : line.substr(end + 1).trim();
|
||||
}
|
||||
|
||||
function parseGroup(line: string) {
|
||||
let name: string = '';
|
||||
const master: string[] = [];
|
||||
const flags: { [k: string]: string | boolean } = {};
|
||||
|
||||
line = line.replace(reCollapseEqual, '=');
|
||||
|
||||
const lm = line.match(reCurlyWhitespace) as RegExpMatchArray;
|
||||
for (let j = 1; j < lm.length; ++j) {
|
||||
const e = lm[j];
|
||||
if (e[0] === '{') {
|
||||
name = e.substring(1, e.length - 1);
|
||||
} else {
|
||||
const es = e.split('=');
|
||||
if (es.length === 2) {
|
||||
if (es[0] === 'master') {
|
||||
master.push(es[1].replace(reTrimCurly, ''));
|
||||
} else {
|
||||
flags[es[0]] = es[1].replace(reTrimCurly, '');
|
||||
}
|
||||
} else {
|
||||
flags[es[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
groupName: name,
|
||||
groupFlags: flags,
|
||||
groupMasters: master,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePointmaster(line: string) {
|
||||
let name: string = '';
|
||||
const tags: string[] = [];
|
||||
let on: boolean | undefined = undefined;
|
||||
|
||||
// Find the string name between curly braces, or print an error if not found
|
||||
const nameMatch = line.match(/{([^}]+)}/);
|
||||
if (nameMatch) {
|
||||
name = nameMatch[1];
|
||||
|
||||
// Find all characters between the pair of single quotes, which are the tags, and add them to the tags array
|
||||
const tagMatch = line.match(/'([^']+)'/);
|
||||
if (tagMatch) {
|
||||
const tagString: string = tagMatch[1];
|
||||
for (let i = 0; i < tagString.length; i++) {
|
||||
tags.push(tagString[i]);
|
||||
}
|
||||
|
||||
// See if the line contains the word "on" or "off" and set the on variable accordingly
|
||||
if (line.includes(' on')) {
|
||||
on = true;
|
||||
} else if (line.includes(' off')) {
|
||||
on = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('Kinemage: Pointmaster definition missing tags: ' + line);
|
||||
}
|
||||
} else {
|
||||
console.log('Kinemage: Pointmaster definition missing name: ' + line);
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
tags: tags,
|
||||
on: on
|
||||
};
|
||||
}
|
||||
|
||||
function convertKinTriangleArrays(ribbonObject: RibbonObject) {
|
||||
// have to convert ribbons/triangle lists from stripdrawmode to normal drawmode
|
||||
// index [ 0 1 2 3 4 5 6 7 8 91011 ]
|
||||
// label/color/ptm [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ]
|
||||
// convertedindex [ 0 1 2 3 4 5 6 7 8 91011121314151617181920212223242526 ]
|
||||
// index [ 0 1 2 3 4 5 6 7 8 91011121314 ] [ 0 1 2 3 4 5 6 7 8 3 4 5 6 7 8 91011 6 7 8 91011121314 ]
|
||||
// position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ]
|
||||
const { labelArray, positionArray, colorArray, breakArray } = ribbonObject;
|
||||
const convertedLabels = [];
|
||||
for (let i = 0; i < (labelArray.length - 2) * 3; ++i) {
|
||||
convertedLabels[i] = labelArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedColors = [];
|
||||
for (let i = 0; i < (colorArray.length - 2) * 3; ++i) {
|
||||
convertedColors[i] = colorArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedPMs = [];
|
||||
for (let i = 0; i < (ribbonObject.pointmasterArray.length - 2) * 3; ++i) {
|
||||
convertedPMs[i] = ribbonObject.pointmasterArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedBreaks = [];
|
||||
for (let i = 0; i < (breakArray.length - 2) * 3; ++i) {
|
||||
convertedBreaks[i] = breakArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedPositions = [];
|
||||
for (let i = 0; i < (positionArray.length / 3 - 2) * 9; ++i) {
|
||||
convertedPositions[i] = positionArray[i - Math.floor(i / 9) * 6];
|
||||
}
|
||||
const vector3Positions = [];
|
||||
for (let i = 0; i < (convertedPositions.length) / 3; ++i) {
|
||||
vector3Positions.push([convertedPositions[i * 3], convertedPositions[i * 3] + 1, convertedPositions[i * 3] + 2]);
|
||||
}
|
||||
return {
|
||||
group: ribbonObject.group,
|
||||
subgroup: ribbonObject.subgroup,
|
||||
name: ribbonObject.name,
|
||||
masterArray: ribbonObject.masterArray,
|
||||
pointmasterArray: convertedPMs,
|
||||
nobutton: ribbonObject.nobutton,
|
||||
labelArray: convertedLabels,
|
||||
positionArray: convertedPositions,
|
||||
breakArray: convertedBreaks,
|
||||
colorArray: convertedColors,
|
||||
pairTriangleNormals: ribbonObject.pairTriangleNormals
|
||||
};
|
||||
}
|
||||
|
||||
function removePointBreaksTriangleArrays(convertedRibbonObject: RibbonObject) {
|
||||
// after converting ribbon/triangle arrys to drawmode, removed point break triangles
|
||||
// label/color [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ]
|
||||
// position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ]
|
||||
const { labelArray, positionArray, colorArray, breakArray } = convertedRibbonObject;
|
||||
const editedLabels = [];
|
||||
const editedPositions = [];
|
||||
const editedColors = [];
|
||||
const editedPMs = [];
|
||||
const editedBreaks = [];
|
||||
for (let i = 0; i < breakArray.length / 3; i++) {
|
||||
const breakPointer = i * 3;
|
||||
const positionPointer = i * 9;
|
||||
if (!breakArray[breakPointer + 1] && !breakArray[breakPointer + 2]) {
|
||||
editedLabels.push(labelArray[breakPointer]);
|
||||
editedLabels.push(labelArray[breakPointer + 1]);
|
||||
editedLabels.push(labelArray[breakPointer + 2]);
|
||||
editedBreaks.push(breakArray[breakPointer]);
|
||||
editedBreaks.push(breakArray[breakPointer + 1]);
|
||||
editedBreaks.push(breakArray[breakPointer + 2]);
|
||||
editedPositions.push(positionArray[positionPointer]);
|
||||
editedPositions.push(positionArray[positionPointer + 1]);
|
||||
editedPositions.push(positionArray[positionPointer + 2]);
|
||||
editedPositions.push(positionArray[positionPointer + 3]);
|
||||
editedPositions.push(positionArray[positionPointer + 4]);
|
||||
editedPositions.push(positionArray[positionPointer + 5]);
|
||||
editedPositions.push(positionArray[positionPointer + 6]);
|
||||
editedPositions.push(positionArray[positionPointer + 7]);
|
||||
editedPositions.push(positionArray[positionPointer + 8]);
|
||||
editedColors.push(colorArray[breakPointer]);
|
||||
editedColors.push(colorArray[breakPointer + 1]);
|
||||
editedColors.push(colorArray[breakPointer + 2]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 1]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 2]);
|
||||
} else {
|
||||
// console.log('X triangle break found')
|
||||
// console.log('skipping: '+positionArray[positionPointer]+','+positionArray[positionPointer+1]+','+positionArray[positionPointer+2]+','
|
||||
// +positionArray[positionPointer+3]+','+positionArray[positionPointer+4]+','+positionArray[positionPointer+5]+','
|
||||
// +positionArray[positionPointer+6]+','+positionArray[positionPointer+7]+','+positionArray[positionPointer+8])
|
||||
}
|
||||
}
|
||||
return {
|
||||
group: convertedRibbonObject.group,
|
||||
subgroup: convertedRibbonObject.subgroup,
|
||||
name: convertedRibbonObject.name,
|
||||
masterArray: convertedRibbonObject.masterArray,
|
||||
pointmasterArray: editedPMs,
|
||||
nobutton: convertedRibbonObject.nobutton,
|
||||
labelArray: editedLabels,
|
||||
positionArray: editedPositions,
|
||||
breakArray: editedBreaks,
|
||||
colorArray: editedColors,
|
||||
pairTriangleNormals: convertedRibbonObject.pairTriangleNormals
|
||||
};
|
||||
}
|
||||
|
||||
class KinParser {
|
||||
// @brief Property that is filled in by the constructor as it parses the file. Read by the caller.
|
||||
kinemage: Kinemage;
|
||||
|
||||
// @brief Constructor for the KinParser class.
|
||||
// @param data The string data to be parsed, including all lines in the file.
|
||||
constructor(data: string) {
|
||||
this._parse(data);
|
||||
}
|
||||
|
||||
private _parse(data: string) {
|
||||
// http://kinemage.biochem.duke.edu/software/king.php
|
||||
|
||||
const kinemage: Kinemage = {
|
||||
comments: [],
|
||||
kinemage: undefined,
|
||||
onewidth: undefined,
|
||||
viewDict: {},
|
||||
pdbfile: undefined,
|
||||
texts: [],
|
||||
text: '',
|
||||
captions: [],
|
||||
caption: '',
|
||||
groupDict: {},
|
||||
subgroupDict: {},
|
||||
masterDict: {},
|
||||
pointmasterDict: {},
|
||||
dotLists: [],
|
||||
vectorLists: [],
|
||||
ballLists: [],
|
||||
ribbonLists: [],
|
||||
groupsAnimate: [],
|
||||
activeAnimateGroup: -1,
|
||||
groupsAnimate2: [],
|
||||
activeAnimateGroup2: -1
|
||||
};
|
||||
this.kinemage = kinemage;
|
||||
|
||||
// Keep a local copy of the ColorDict that we can update with new colors defined in the file.
|
||||
const localColorDict: { [k: string]: Color } = Object.assign({}, ColorDict);
|
||||
|
||||
let currentGroup: string = '';
|
||||
let currentGroupMasters: string[];
|
||||
let currentSubgroup: string = '';
|
||||
let currentSubgroupMasters: string[];
|
||||
|
||||
let isDotList = false;
|
||||
let prevDotLabel = '';
|
||||
let dotDefaultColor: Color;
|
||||
let dotLabel: string[], dotPosition: number[], dotColor: Color[], dotPointMasters: string[][];
|
||||
|
||||
let isVectorList = false;
|
||||
let prevVecLabel = '';
|
||||
let prevVecPosition: number[] | null = null;
|
||||
let prevVecColor: Color | null = null;
|
||||
let vecDefaultColor: Color, vecDefaultWidth: number;
|
||||
let vecLabel1: string[], vecLabel2: string[], vecPosition1: number[], vecPosition2: number[], vecColor1: Color[], vecColor2: Color[];
|
||||
let vecWidth: number[], vecPointMasters: string[][];
|
||||
|
||||
let isBallList = false;
|
||||
let prevBallLabel = '';
|
||||
let ballRadius: number[], ballDefaultColor: Color, ballDefaultRadius: number;
|
||||
let ballLabel: string[], ballPosition: number[], ballColor: Color[], ballPointMasters: string[][];
|
||||
|
||||
let isRibbonList = false;
|
||||
let ribbonIsTriangles = false;
|
||||
let prevRibbonPointLabel = '';
|
||||
|
||||
let ribbonListDefaultColor: Color = localColorDict['white'];
|
||||
let ribbonPointLabelArray: string[], ribbonPointPositionArray: number[], ribbonPointBreakArray: boolean[], ribbonPointColorArray: Color[];
|
||||
let ribbonPointMasters: string[][];
|
||||
|
||||
let isText = false;
|
||||
let isCaption = false;
|
||||
|
||||
let foundAnimate = false;
|
||||
let found2Animate = false;
|
||||
|
||||
function _parseChunkOfLines(_i: number, _n: number, lines: string[]) {
|
||||
for (let i = _i; i < _n; ++i) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line[0] === '@') {
|
||||
isDotList = false;
|
||||
isVectorList = false;
|
||||
isBallList = false;
|
||||
isRibbonList = false;
|
||||
isText = false;
|
||||
isCaption = false;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
isDotList = false;
|
||||
isVectorList = false;
|
||||
isBallList = false;
|
||||
isRibbonList = false;
|
||||
} else if (line.startsWith('@dot') /* dot or dotlist */) {
|
||||
// @dotlist {x} color=white master={vdw contact} master={dots}
|
||||
|
||||
let { listColor, listName, listMasters, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
isDotList = true;
|
||||
prevDotLabel = '';
|
||||
dotLabel = [];
|
||||
dotPosition = [];
|
||||
dotColor = [];
|
||||
dotPointMasters = [];
|
||||
dotDefaultColor = listColor;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.dotLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: dotPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: dotLabel,
|
||||
positionArray: dotPosition,
|
||||
colorArray: dotColor
|
||||
});
|
||||
} else if (line.startsWith('@vector') /* vector or vectorlist */) {
|
||||
// @vectorlist {x} color=white master={small overlap} master={dots}
|
||||
|
||||
let { listMasters, listName, listWidth, listColor, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isVectorList = true;
|
||||
prevVecLabel = '';
|
||||
prevVecPosition = null;
|
||||
prevVecColor = null;
|
||||
vecLabel1 = [];
|
||||
vecLabel2 = [];
|
||||
vecPosition1 = [];
|
||||
vecPosition2 = [];
|
||||
vecColor1 = [];
|
||||
vecColor2 = [];
|
||||
vecWidth = [];
|
||||
vecDefaultColor = listColor;
|
||||
vecPointMasters = [];
|
||||
vecDefaultWidth = 2;
|
||||
if (listWidth) {
|
||||
vecDefaultWidth = listWidth;
|
||||
}
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.vectorLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: vecPointMasters,
|
||||
nobutton: nobutton,
|
||||
label1Array: vecLabel1,
|
||||
label2Array: vecLabel2,
|
||||
position1Array: vecPosition1,
|
||||
position2Array: vecPosition2,
|
||||
color1Array: vecColor1,
|
||||
color2Array: vecColor2,
|
||||
width: vecWidth
|
||||
});
|
||||
} else if (line.startsWith('@ball') /* ball or balllist*/ || line.startsWith('@sphere') /* sphere or spherelist */) {
|
||||
let { listName, listColor, listMasters, listRadius, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isBallList = true;
|
||||
|
||||
prevBallLabel = '';
|
||||
ballLabel = [];
|
||||
ballRadius = [];
|
||||
ballPosition = [];
|
||||
ballColor = [];
|
||||
ballPointMasters = [];
|
||||
ballDefaultColor = listColor;
|
||||
ballDefaultRadius = listRadius !== undefined ? listRadius : 1;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.ballLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: ballPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: ballLabel,
|
||||
radiusArray: ballRadius,
|
||||
positionArray: ballPosition,
|
||||
colorArray: ballColor
|
||||
});
|
||||
} else if (line.startsWith('@ribbon') /* ribbon or ribbonlist */ || line.startsWith('@triangle') /* triangle or trianglelist */) {
|
||||
let { listMasters, listName, listColor, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
isRibbonList = true;
|
||||
ribbonIsTriangles = line.startsWith('@triangle'); /* triangle or trianglelist */
|
||||
prevRibbonPointLabel = '';
|
||||
ribbonPointLabelArray = [];
|
||||
ribbonPointPositionArray = [];
|
||||
ribbonPointBreakArray = [];
|
||||
ribbonPointColorArray = [];
|
||||
ribbonListDefaultColor = listColor;
|
||||
ribbonPointMasters = [];
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.ribbonLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: ribbonPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: ribbonPointLabelArray,
|
||||
positionArray: ribbonPointPositionArray,
|
||||
breakArray: ribbonPointBreakArray,
|
||||
colorArray: ribbonPointColorArray,
|
||||
pairTriangleNormals: !ribbonIsTriangles
|
||||
});
|
||||
} else if (line.startsWith('@text')) {
|
||||
isText = true;
|
||||
kinemage.texts.push(line.substr(5));
|
||||
} else if (line.startsWith('@caption')) {
|
||||
isCaption = true;
|
||||
kinemage.captions.push(line.substr(8));
|
||||
} else if (isDotList) {
|
||||
// { CB THR 1 A}sky 'P' 18.915,14.199,5.024
|
||||
|
||||
let { label, color, position, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevDotLabel;
|
||||
} else {
|
||||
prevDotLabel = label;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = dotDefaultColor;
|
||||
}
|
||||
|
||||
dotLabel.push(label);
|
||||
dotPosition.push(...position);
|
||||
dotColor.push(color);
|
||||
dotPointMasters.push(pointMasters);
|
||||
} else if (isVectorList) {
|
||||
// { n thr A 1 B13.79 1crnFH} P 17.047, 14.099, 3.625 { n thr A 1 B13.79 1crnFH} L 17.047, 14.099, 3.625
|
||||
|
||||
const doubleLine = line.replace(/(?!^){/g, '\n{');
|
||||
const splitLine = doubleLine.split(/\n/);
|
||||
|
||||
for (let i2 = 0; i2 < splitLine.length; i2++) {
|
||||
const singlePointLine = splitLine[i2];
|
||||
let { label, color, width, position, isLineBreak, pointMasters } = parseListElm(singlePointLine, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevVecLabel;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = vecDefaultColor;
|
||||
}
|
||||
|
||||
if (!isLineBreak) {
|
||||
if (prevVecPosition !== null) {
|
||||
if (width === undefined) {
|
||||
width = vecDefaultWidth;
|
||||
}
|
||||
|
||||
vecLabel1.push(prevVecLabel);
|
||||
vecPosition1.push(...prevVecPosition);
|
||||
vecColor1.push(prevVecColor ? prevVecColor : vecDefaultColor);
|
||||
|
||||
vecLabel2.push(label);
|
||||
vecPosition2.push(...position);
|
||||
vecColor2.push(color);
|
||||
vecWidth.push(width);
|
||||
|
||||
vecPointMasters.push(pointMasters);
|
||||
}
|
||||
}
|
||||
|
||||
prevVecLabel = label;
|
||||
prevVecPosition = position;
|
||||
prevVecColor = color;
|
||||
}
|
||||
} else if (isBallList) {
|
||||
// {cb arg A 1 1.431 -106.80} r=1.431 39.085, 8.083, 22.182
|
||||
|
||||
let { label, radius, color, position, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevBallLabel;
|
||||
} else {
|
||||
prevBallLabel = label;
|
||||
}
|
||||
|
||||
if (radius === undefined) {
|
||||
radius = ballDefaultRadius;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = ballDefaultColor;
|
||||
}
|
||||
|
||||
ballLabel.push(label);
|
||||
ballRadius.push(radius);
|
||||
ballPosition.push(...position);
|
||||
ballColor.push(color);
|
||||
ballPointMasters.push(pointMasters);
|
||||
} else if (isRibbonList) {
|
||||
let { label, color, position, isTriangleBreak, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevRibbonPointLabel;
|
||||
} else {
|
||||
prevRibbonPointLabel = label;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = ribbonListDefaultColor;
|
||||
}
|
||||
|
||||
ribbonPointLabelArray.push(label);
|
||||
ribbonPointPositionArray.push(...position);
|
||||
ribbonPointBreakArray.push(isTriangleBreak);
|
||||
ribbonPointColorArray.push(color);
|
||||
ribbonPointMasters.push(pointMasters);
|
||||
} else if (isText) {
|
||||
kinemage.texts.push(line);
|
||||
} else if (isCaption) {
|
||||
kinemage.captions.push(line);
|
||||
} else if (line.startsWith('@kinemage')) {
|
||||
kinemage.kinemage = parseInt(line.substr(9).trim());
|
||||
} else if (line.startsWith('@onewidth')) {
|
||||
kinemage.onewidth = true;
|
||||
} else if (line.startsWith('@pdbfile')) {
|
||||
kinemage.pdbfile = parseStr(line);
|
||||
} else if (line.startsWith('@group')) {
|
||||
const { groupName, groupFlags, groupMasters } = parseGroup(line);
|
||||
if (!kinemage.groupDict[groupName as string]) {
|
||||
kinemage.groupDict[groupName as string] = {
|
||||
dominant: false,
|
||||
// If the groupFlags include animate or 2animate, set those to true in the groupDict. Otherwise, set them to false.
|
||||
animate: groupFlags['animate'] ? true : false,
|
||||
'2animate': groupFlags['2animate'] ? true : false,
|
||||
// If the foundAnimate or found2Animate flags are true, set off to true; otherwise set it to the flags value.
|
||||
off: (foundAnimate || found2Animate) ? true : groupFlags['off'] ? true : false
|
||||
};
|
||||
// If the animate or 2animate flags are found in the groupFlags, set foundAnimate
|
||||
// or found2Animate to true, respectively. Also update the list and index.
|
||||
if (groupFlags['animate']) {
|
||||
foundAnimate = true;
|
||||
kinemage.groupsAnimate.push(groupName as string);
|
||||
kinemage.activeAnimateGroup = 0;
|
||||
}
|
||||
if (groupFlags['2animate']) {
|
||||
found2Animate = true;
|
||||
kinemage.groupsAnimate2.push(groupName as string);
|
||||
kinemage.activeAnimateGroup2 = 0;
|
||||
}
|
||||
currentGroupMasters = groupMasters;
|
||||
}
|
||||
currentGroup = groupName;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
currentGroupMasters.forEach(function (master) {
|
||||
if (!kinemage.masterDict[master]) {
|
||||
kinemage.masterDict[master] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in groupFlags as { [k: string]: boolean }) {
|
||||
kinemage.groupDict[groupName as string][key] = (groupFlags as { [k: string]: boolean })[key];
|
||||
}
|
||||
} else if (line.startsWith('@subgroup')) {
|
||||
const { groupName, groupFlags, groupMasters } = parseGroup(line);
|
||||
|
||||
const combinedName = currentGroup + ':' + groupName as string;
|
||||
if (!kinemage.subgroupDict[combinedName]) {
|
||||
kinemage.subgroupDict[combinedName] = {
|
||||
dominant: false,
|
||||
// If the groupFlag includes "off", set off to true; otherwise, set it to false.
|
||||
off: groupFlags['off'] ? true : false,
|
||||
group: currentGroup
|
||||
};
|
||||
currentSubgroupMasters = groupMasters;
|
||||
}
|
||||
currentSubgroup = combinedName;
|
||||
|
||||
if (currentSubgroupMasters) {
|
||||
currentSubgroupMasters.forEach(function (master) {
|
||||
if (!kinemage.masterDict[master]) {
|
||||
kinemage.masterDict[master] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in groupFlags as { [k: string]: boolean }) {
|
||||
kinemage.subgroupDict[combinedName as string][key] = (groupFlags as { [k: string]: boolean })[key];
|
||||
}
|
||||
} else if (line.startsWith('@master')) {
|
||||
const name = parseStr(line);
|
||||
const flag = parseFlag(line);
|
||||
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: There can be more than one flag on a @master line: indent, off, nobutton
|
||||
if (flag === 'on') {
|
||||
kinemage.masterDict[name].visible = true;
|
||||
} else if (flag === 'off') {
|
||||
kinemage.masterDict[name].visible = false;
|
||||
} else if (flag === 'indent') {
|
||||
kinemage.masterDict[name].indent = true;
|
||||
} else if (!flag) {
|
||||
// nothing to do
|
||||
}
|
||||
} else if (line.startsWith('@pointmaster')) {
|
||||
const { name, tags, on } = parsePointmaster(line);
|
||||
if (name.length > 0 && tags.length > 0) {
|
||||
|
||||
// Ensure that we have a masterDict entry for this pointmaster name, even though it doesn't have any flags of its own.
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: on !== false // If the on variable is explicitly false, set visible to false. Otherwise, set it to true.
|
||||
};
|
||||
}
|
||||
|
||||
// Add the mapping to point each single-character tag to the pointmaster name in the pointmasterDict.
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
kinemage.pointmasterDict[tags[i]] = name;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('@colorset')) {
|
||||
// We have a string inside curly brackets {} followed by the name of an existing dictionary color.
|
||||
const colorName = parseStr(line);
|
||||
const colorReference = parseFlag(line);
|
||||
if (colorReference && colorReference in localColorDict) {
|
||||
localColorDict[colorName] = localColorDict[colorReference];
|
||||
}
|
||||
} else if (/^@(\d*)viewid\b/.test(line)) {
|
||||
const m = line.match(/^@(\d*)viewid\b/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].name = parseStr(line);
|
||||
} else if (/^@(\d*)center\b/.test(line)) {
|
||||
// Match all of the line after center as another string.
|
||||
const m = line.match(/^@(\d*)center\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the three whitespace-separated numbers after the keyword. Parse each as a float and
|
||||
// add them to a length-3 list of numbers.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
// Split on whitespace and take the first three tokens, parsed as floating-point numbers, as the center coordinates.
|
||||
const parts = rest.length > 0 ? rest.split(/\s+/).filter(Boolean) : [];
|
||||
const centerTokens = parts.slice(0, 3).map(parseFloat);
|
||||
// If the length is 3 and all are valid numbers, add the list of three numbers to the view dictionary.
|
||||
if (centerTokens.length === 3 && centerTokens.every(num => !isNaN(num))) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].center = centerTokens;
|
||||
}
|
||||
} else if (/^@(\d*)matrix\b/.test(line)) {
|
||||
// Match all of the line after matrix as another string.
|
||||
const m = line.match(/^@(\d*)matrix\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the nine whitespace-separated numbers after the keyword. Parse each as a float and
|
||||
// add them to a length-9 list of numbers.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
// Split on whitespace and take the first nine tokens, parsed as floating-point numbers, as the matrix values.
|
||||
const parts = rest.length > 0 ? rest.split(/\s+/).filter(Boolean) : [];
|
||||
const matrixTokens = parts.slice(0, 9).map(parseFloat);
|
||||
// If the length is 9 and all are valid numbers, add the list of nine numbers to the view dictionary.
|
||||
if (matrixTokens.length === 9 && matrixTokens.every(num => !isNaN(num))) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].matrix = matrixTokens;
|
||||
}
|
||||
} else if (/^@(\d*)span\b/.test(line)) {
|
||||
// Match all of the line after span as another string.
|
||||
const m = line.match(/^@(\d*)span\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const spanValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(spanValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].span = spanValue;
|
||||
}
|
||||
} else if (/^@(\d*)zoom\b/.test(line)) {
|
||||
// Match all of the line after zoom as another string.
|
||||
const m = line.match(/^@(\d*)zoom\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const zoomValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(zoomValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].zoom = zoomValue;
|
||||
}
|
||||
} else if (/^@(\d*)zslab\b/.test(line)) {
|
||||
// Match all of the line after zslab as another string.
|
||||
const m = line.match(/^@(\d*)zslab\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const zslabValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(zslabValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].zslab = zslabValue;
|
||||
}
|
||||
} else {
|
||||
console.log('Kinemage: Unrecognized line: ' + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break the file into a list of lines and then parse them all.
|
||||
const lines = data.split(/\r?\n/);
|
||||
_parseChunkOfLines(0, lines.length, lines);
|
||||
|
||||
kinemage.text = kinemage.texts.join('\n').trim();
|
||||
kinemage.caption = kinemage.captions.join('\n').trim();
|
||||
if (kinemage.ribbonLists) {
|
||||
const convertedLists: RibbonObject[] = [];
|
||||
kinemage.ribbonLists.forEach(function (listObject) {
|
||||
convertedLists.push(removePointBreaksTriangleArrays(convertKinTriangleArrays(listObject)));
|
||||
});
|
||||
kinemage.ribbonLists = convertedLists;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export { KinParser };
|
||||
41
src/extensions/kinemage/reader/parser.ts
Normal file
41
src/extensions/kinemage/reader/parser.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { ReaderResult as Result } from '../../../mol-io/reader/result';
|
||||
import { Task, RuntimeContext } from '../../../mol-task';
|
||||
import { Kinemage } from './schema';
|
||||
import { KinParser } from './kinparser';
|
||||
|
||||
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Kinemage[]>> {
|
||||
const kinemages: Kinemage[] = [];
|
||||
// Split the data into sections based on the '@kinemage' keyword, which indicates one or more kinemages in the file.
|
||||
// Handle the case where there is no '@kinemage' keyword by parsing the entire file.
|
||||
const kinemageSections = data.split(/@kinemage\s+\d+/); // Split based on '@kinemage' keyword followed by a number
|
||||
|
||||
// If there are one or more @kinemage sections, ignore the portion before the first one.
|
||||
// This will either be an empty string (if the first section starts at the beginning of the file)
|
||||
// or header data that is not part of a particular kinemage. This has the effect of removing
|
||||
// the header data even in the case where there is a single @kinemage keyword.
|
||||
if (kinemageSections.length > 1) {
|
||||
kinemageSections.shift();
|
||||
}
|
||||
|
||||
for (const section of kinemageSections) {
|
||||
if (section.trim()) { // Ignore empty sections
|
||||
const NGLParser = new KinParser(section.trim());
|
||||
const kinData = NGLParser.kinemage;
|
||||
kinemages.push(kinData);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(kinemages);
|
||||
}
|
||||
|
||||
export function parseKin(data: string) {
|
||||
return Task.create<Result<Kinemage[]>>('Parse KIN', async ctx => {
|
||||
return await parseInternal(data, ctx);
|
||||
});
|
||||
}
|
||||
82
src/extensions/kinemage/reader/schema.ts
Normal file
82
src/extensions/kinemage/reader/schema.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
export interface Kinemage {
|
||||
readonly comments: ReadonlyArray<string>
|
||||
kinemage?: number,
|
||||
onewidth?: any,
|
||||
viewDict: { [id: number]: View },
|
||||
pdbfile?: string,
|
||||
text: string,
|
||||
texts: string[],
|
||||
captions: string[],
|
||||
caption: string,
|
||||
groupDict: { [k: string]: { [k: string]: boolean } },
|
||||
subgroupDict: { [k: string]: any }, // /< Subgroup key is "GroupName:SubgroupName" to preserve tree structure
|
||||
masterDict: { [k: string]: { indent: boolean, visible: boolean } },
|
||||
pointmasterDict: { [k: string]: string }, // /< Maps from single-character name to master name for points, e.g. 'a' -> 'alta'
|
||||
dotLists: DotList[],
|
||||
vectorLists: VectorList[],
|
||||
ballLists: BallList[],
|
||||
ribbonLists: RibbonObject[],
|
||||
groupsAnimate: string[],
|
||||
activeAnimateGroup: number,
|
||||
groupsAnimate2: string[],
|
||||
activeAnimateGroup2: number,
|
||||
viewSnapshots?: {} // /< Used to store view snapshots in behavior.ts to use in ui.tsx
|
||||
}
|
||||
|
||||
/** Common base for all list-like objects in a kinemage */
|
||||
export interface KinListBase {
|
||||
name?: string, // /< Optional name of the whole List
|
||||
group: string, // /< Name of the group this List belongs to (may be '' if no group)
|
||||
subgroup: string, // /< Name of the subgroup this List belongs to (may be '' if no subgroup)
|
||||
nobutton: boolean, // /< Whether the list is a nobutton list (true if 'nobutton' keyword found)
|
||||
masterArray: any[], // /< Array of master names per List, not per element
|
||||
pointmasterArray: string[][] // /< Array of point master names per element
|
||||
}
|
||||
|
||||
export interface DotList extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
colorArray: Color[] // /< Color for each element, as many as elements
|
||||
}
|
||||
|
||||
export interface BallList extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
colorArray: Color[], // /< Color for each element, as many as elements
|
||||
radiusArray: number[] // /< A single radius per element
|
||||
}
|
||||
|
||||
export interface RibbonObject extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 9x as many as triangles (3 vertices per triangle)
|
||||
colorArray: Color[], // /< Color for each element, as many as elements
|
||||
breakArray: boolean[], // /< A single boolean per element indicating if there is a break there
|
||||
pairTriangleNormals: boolean // /< Whether to pair every other triangle normal for lighting (true for ribbons, false for triangles)
|
||||
}
|
||||
|
||||
export interface VectorList extends KinListBase {
|
||||
label1Array: string[], // /< Array of labels for the first half of each element
|
||||
label2Array: string[], // /< Array of labels for the second half of each element
|
||||
position1Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
position2Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
color1Array: Color[], // /< Color for first half of each element, as many as elements
|
||||
color2Array: Color[], // /< Color for second half of each element, as many as elements
|
||||
width: number[] // /< A single width per element
|
||||
}
|
||||
|
||||
export interface View {
|
||||
name?: string, // /< Optional name of the View
|
||||
center?: number[], // /< X, Y, Z of the center of the view; the model rotates around this point
|
||||
matrix?: number[], // /< Specifies and orthonormal rotation matrix defining view orientation
|
||||
span?: number, // /< Specifies the (smaller of) width or height of the view in world coordinates at the center
|
||||
zoom?: number, // /< Alternate zoom specification, indicates how much of the model is visible, 1=all, 2=half
|
||||
zslab?: number // /< Distance from the center to the near and far clipping planes, 200 means same as span (half is percent of half span)
|
||||
}
|
||||
34
src/extensions/kinemage/representation.ts
Normal file
34
src/extensions/kinemage/representation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../mol-repr/representation';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
|
||||
import { ThemeRegistryContext } from '../../mol-theme/theme';
|
||||
|
||||
// TODO: Convert this approach to a more usual one that creates visuals during parse and shows them
|
||||
// during visuals.
|
||||
|
||||
const KinemageDataVisuals = {
|
||||
};
|
||||
|
||||
export const KinemageDataParams = {
|
||||
visuals: PD.MultiSelect([], PD.objectToOptions(KinemageDataVisuals)),
|
||||
};
|
||||
export type KinemageDataParams = typeof KinemageDataParams
|
||||
export type KinemageDataProps = PD.Values<KinemageDataParams>
|
||||
|
||||
export function getKinemageDataParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
return PD.clone(KinemageDataParams);
|
||||
}
|
||||
|
||||
export type KinemageDataRepresentation = StructureRepresentation<KinemageDataParams>
|
||||
export function KinemageDataRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, KinemageDataParams>): KinemageDataRepresentation {
|
||||
return Representation.createMulti('Membrane Orientation', ctx, getParams, StructureRepresentationStateBuilder, KinemageDataVisuals as unknown as Representation.Def<Structure, KinemageDataParams>);
|
||||
}
|
||||
305
src/extensions/kinemage/ui.tsx
Normal file
305
src/extensions/kinemage/ui.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Kinemage right-panel controls (right-panel only).
|
||||
*
|
||||
* Shows kinemage views, animate buttons, and group/subgroup/master toggles in the right inspector.
|
||||
* Controls directly operate on the loaded kinemage runtime data and call exported helpers
|
||||
* to rebuild visuals. No State Tree JSON nodes are created for these UI items.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableState, CollapsableControls } from '../../mol-plugin-ui/base';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { applyViewSnapshot, rebuildShapesForKinemage } from './behavior';
|
||||
import { Kinemage } from './reader/schema';
|
||||
|
||||
interface KinemageControlState extends CollapsableState {
|
||||
isBusy: boolean
|
||||
}
|
||||
|
||||
function nameFromString(s: string | undefined) {
|
||||
// If this is undefined, return undefined.
|
||||
if (!s) return undefined;
|
||||
// Return up to the first 30 characters of the string.
|
||||
return s.length > 30 ? s.substring(0, 30) + '...' : s;
|
||||
}
|
||||
|
||||
export class KinemageControls extends CollapsableControls<{}, KinemageControlState> {
|
||||
protected defaultState(): KinemageControlState {
|
||||
return {
|
||||
header: 'Kinemage',
|
||||
isCollapsed: false,
|
||||
isBusy: false,
|
||||
// default hidden until a kinemage is present
|
||||
isHidden: true,
|
||||
brand: { accent: 'cyan', svg: undefined as any }
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
|
||||
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
|
||||
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
|
||||
// also track cell state updates that may change labels / visibility
|
||||
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
|
||||
|
||||
// ensure initial visibility reflects current state
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellCreated(e: any) {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellRemoved() {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private updateVisibility() {
|
||||
const kinemages = this.getKinemageList();
|
||||
this.setState({ isHidden: kinemages.length === 0 });
|
||||
}
|
||||
|
||||
private getKinemageList(): Array<{ kinData: Kinemage, ref: string }> {
|
||||
const result: Array<{ kinData: Kinemage, ref: string }> = [];
|
||||
|
||||
try {
|
||||
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
|
||||
for (const [ref, entry] of cells) {
|
||||
const obj = (entry as any).obj;
|
||||
// Look for Format.Json nodes that contain kinData
|
||||
if (obj && obj.data && (obj.data as any).kinData) {
|
||||
result.push({ kinData: (obj.data as any).kinData, ref });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to enumerate kinemage nodes', e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async applyView(kinData: Kinemage, viewKey: string) {
|
||||
const snap = (kinData as any).viewSnapshots?.[viewKey];
|
||||
if (snap) {
|
||||
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
|
||||
}
|
||||
}
|
||||
|
||||
private async toggleVisibility(kinData: Kinemage, kinRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
|
||||
try {
|
||||
if (target.type === 'group') {
|
||||
const g = kinData.groupDict[target.key];
|
||||
if (g) g.off = !g.off;
|
||||
} else if (target.type === 'subgroup') {
|
||||
const s = kinData.subgroupDict[target.key];
|
||||
if (s) s.off = !s.off;
|
||||
} else {
|
||||
const m = kinData.masterDict[target.key];
|
||||
if (m) m.visible = !m.visible;
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle kinemage visibility', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerAnimateForKin(kinData: Kinemage, kinRef: string, mode: 'animate' | '2animate') {
|
||||
try {
|
||||
if (mode === 'animate') {
|
||||
kinData.activeAnimateGroup = (kinData.activeAnimateGroup + 1) % Math.max(1, kinData.groupsAnimate.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate.length; i++) {
|
||||
const groupName = kinData.groupsAnimate[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kinData.activeAnimateGroup2 = (kinData.activeAnimateGroup2 + 1) % Math.max(1, kinData.groupsAnimate2.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate2.length; i++) {
|
||||
const groupName = kinData.groupsAnimate2[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger animate', e);
|
||||
}
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
const kins = this.getKinemageList();
|
||||
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
|
||||
|
||||
const blocks: React.ReactNode[] = [];
|
||||
for (const { kinData, ref } of kins) {
|
||||
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
|
||||
const kinBlock: React.ReactNode[] = [];
|
||||
|
||||
// Title
|
||||
kinBlock.push(
|
||||
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
// views
|
||||
const viewEntries = Object.entries(kinData.viewDict || {});
|
||||
if (viewEntries.length > 0) {
|
||||
for (const [viewKey, viewObj] of viewEntries) {
|
||||
const label = `View ${viewObj.name || `View ${viewKey}`}`;
|
||||
kinBlock.push(
|
||||
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.applyView(kinData, viewKey)}
|
||||
title={`Apply view: ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// animate
|
||||
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, 'animate')}
|
||||
title='Cycle through animation frames'
|
||||
>
|
||||
Animate
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, '2animate')}
|
||||
title='Cycle through second animation frames'
|
||||
>
|
||||
Animate2
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// groups
|
||||
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
|
||||
if (!(groupInfo as any).nobutton) {
|
||||
const visible = !(groupInfo as any).off;
|
||||
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
|
||||
const isAnimate = kinData.groupsAnimate?.includes(groupKey) || kinData.groupsAnimate2?.includes(groupKey);
|
||||
const label = isAnimate ? `* ${groupKey}` : groupKey;
|
||||
kinBlock.push(
|
||||
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'group', key: groupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={label}>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
|
||||
if (!(groupInfo as any).dominant) {
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
if (subgroupKey.startsWith(groupKey + ':')) {
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
const subgroupLabel = subgroupKey.split(':')[1];
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupLabel}>{subgroupLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subgroups that don't belong to a group (standalone)
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
// if parent group present, those groups' subgroups are already shown when iterating groups
|
||||
if (subgroupKey.indexOf(':') !== -1) {
|
||||
// subgroups with parent group; skip here (shown under parent group)
|
||||
continue;
|
||||
}
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupKey}>{subgroupKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// masters
|
||||
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict || {})) {
|
||||
const visible = !!(masterInfo && (masterInfo as any).visible);
|
||||
kinBlock.push(
|
||||
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'master', key: masterKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={masterKey}>{masterKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
|
||||
}
|
||||
|
||||
return <>{blocks}</>;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { CameraFogParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
|
||||
import { BackgroundParams } from '../../mol-canvas3d/passes/background';
|
||||
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
|
||||
@@ -15,27 +15,23 @@ import { OutlineParams } from '../../mol-canvas3d/passes/outline';
|
||||
import { ShadowParams } from '../../mol-canvas3d/passes/shadow';
|
||||
import { SsaoParams } from '../../mol-canvas3d/passes/ssao';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { StateObjectSelector, StateTransform } from '../../mol-state';
|
||||
import { fovAdjustedPosition } from '../../mol-util/camera';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { deepClone } from '../../mol-util/object';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { SnapshotMetadata } from './mvs-data';
|
||||
import { MVSAnimationNode } from './tree/animation/animation-tree';
|
||||
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
import { Vector3 } from './tree/mvs/param-types';
|
||||
|
||||
|
||||
const DefaultFocusOptions = {
|
||||
minRadius: 5,
|
||||
extraRadius: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
|
||||
|
||||
@@ -74,31 +70,20 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is undefined).
|
||||
* Orient the camera based on a focus node params. **/
|
||||
export async function setFocus(plugin: PluginContext, focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]) {
|
||||
const snapshot = getFocusSnapshot(plugin, {
|
||||
...snapshotFocusInfoFromMvsFocuses(focuses),
|
||||
minRadius: DefaultFocusOptions.minRadius,
|
||||
});
|
||||
if (!snapshot) return;
|
||||
resetSceneRadiusFactor(plugin);
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
|
||||
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector | undefined, params: MolstarNodeParams<'focus'> & { center?: Vector3 } }[], ignoreOrientation: boolean): PluginState.SnapshotFocusInfo {
|
||||
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
|
||||
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
|
||||
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
|
||||
return {
|
||||
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
|
||||
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
|
||||
targetRef: f.target?.ref === StateTransform.RootRef ? undefined : f.target?.ref, // need to treat root separately so it does not include invisible structure parts etc.
|
||||
center: f.params.center ? Vec3.create(...f.params.center) : undefined,
|
||||
radius: f.params.radius ?? undefined,
|
||||
radiusFactor: f.params.radius_factor,
|
||||
extraRadius: f.params.radius_extent,
|
||||
})),
|
||||
direction: Vec3.create(...direction),
|
||||
up: Vec3.create(...up),
|
||||
direction: ignoreOrientation ? undefined : Vec3.create(...direction),
|
||||
up: ignoreOrientation ? undefined : Vec3.create(...up),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,24 +96,34 @@ function adjustSceneRadiusFactor(plugin: PluginContext, cameraTarget: Vec3 | und
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
|
||||
/** Reset `sceneRadiusFactor` property to the default value */
|
||||
function resetSceneRadiusFactor(plugin: PluginContext) {
|
||||
const sceneRadiusFactor = Canvas3DParams.sceneRadiusFactor.defaultValue;
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
|
||||
/** Create object for PluginState.Snapshot.camera based on tree loading context and MVS snapshot metadata */
|
||||
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }): PluginState.Snapshot['camera'] {
|
||||
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, options: { previousTransitionDurationMs?: number, ignoreCameraOrientation?: boolean }): PluginState.Snapshot['camera'] {
|
||||
const camera: PluginState.Snapshot['camera'] = {
|
||||
transitionStyle: 'animate',
|
||||
transitionDurationInMs: metadata.previousTransitionDurationMs ?? 0,
|
||||
transitionDurationInMs: options.previousTransitionDurationMs ?? 0,
|
||||
};
|
||||
if (context.camera.cameraParams !== undefined) {
|
||||
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
|
||||
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, context.camera.cameraParams);
|
||||
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
|
||||
const cam = context.camera.cameraParams;
|
||||
if (options.ignoreCameraOrientation) {
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses([{
|
||||
target: undefined,
|
||||
params: {
|
||||
center: cam.target,
|
||||
radius: Vec3.distance(cam.target as number[] as Vec3, cam.position as number[] as Vec3) / 2,
|
||||
direction: MVSTreeSchema.nodes.focus.params.fields.direction.default, // will be ignored
|
||||
up: MVSTreeSchema.nodes.focus.params.fields.up.default, // will be ignored
|
||||
radius_factor: 1, // will be ignored
|
||||
radius_extent: 0, // will be ignored
|
||||
},
|
||||
}], true);
|
||||
// This will not work exactly when viewport height>width because of how focusing works (could be solved by adjusting radius by aspect ration, but that would mess up cropping, and wouldn't work properly when aspect ration changes after loading)
|
||||
} else {
|
||||
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
|
||||
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, cam);
|
||||
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
|
||||
}
|
||||
} else {
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses);
|
||||
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses, options.ignoreCameraOrientation ?? false);
|
||||
}
|
||||
return camera;
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
|
||||
return {
|
||||
factory: MVSAnnotationColorTheme,
|
||||
granularity: 'groupInstance',
|
||||
preferSmoothing: true,
|
||||
preferSmoothing: false,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Assigns colors based on custom MolViewSpec annotation data.',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -9,16 +9,19 @@ import { TextBuilder } from '../../../../mol-geo/geometry/text/text-builder';
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { ComplexTextVisual, ComplexVisual } from '../../../../mol-repr/structure/complex-visual';
|
||||
import * as Original from '../../../../mol-repr/structure/visual/label-text';
|
||||
import { ElementIterator, eachSerialElement, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { eachSerialElement, ElementIterator, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { VisualUpdateState } from '../../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../../mol-repr/visual';
|
||||
import { Theme } from '../../../../mol-theme/theme';
|
||||
import { arrayEqual } from '../../../../mol-util';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { omitObjectKeys } from '../../../../mol-util/object';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { FormatTemplate } from '../../../../mol-util/string-format';
|
||||
import { textPropsForSelection } from '../../helpers/label-text';
|
||||
import { groupRows } from '../../helpers/selections';
|
||||
import { getMVSAnnotationForStructure } from '../annotation-prop';
|
||||
import { MVSAnnotationRow } from '../../helpers/schemas';
|
||||
import { GroupedArray } from '../../helpers/utils';
|
||||
import { getMVSAnnotationForStructure, MVSAnnotation } from '../annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for "label-text" visual in "MVS Annotation Label" representation */
|
||||
@@ -26,6 +29,8 @@ export type MVSAnnotationLabelTextParams = typeof MVSAnnotationLabelTextParams
|
||||
export const MVSAnnotationLabelTextParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property', isEssential: true }),
|
||||
fieldName: PD.Text('label', { description: 'Annotation field (column) from which to take label contents', isEssential: true }),
|
||||
textFormat: PD.Text('{}', { description: 'Formatting template for the label text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, label will not be displayed.', isEssential: true }),
|
||||
groupByFields: PD.ObjectList({ fieldName: PD.Text(), }, obj => obj.fieldName, { defaultValue: [{ fieldName: 'group_id' }], description: 'Set of annotation fields for grouping annotation rows into label instances (i.e. annotation rows with the same values in all group-by fields will yield one label instance). Annotation row with undefined value in any group-by field is considered a separate label instance.', isEssential: true }),
|
||||
...omitObjectKeys(Original.LabelTextParams, ['level', 'chainScale', 'residueScale', 'elementScale']),
|
||||
borderColor: { ...Original.LabelTextParams.borderColor, defaultValue: ColorNames.black },
|
||||
};
|
||||
@@ -42,7 +47,11 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
|
||||
getLoci: getSerialElementLoci,
|
||||
eachLocation: eachSerialElement,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MVSAnnotationLabelTextParams>, currentProps: PD.Values<MVSAnnotationLabelTextParams>) => {
|
||||
state.createGeometry = newProps.annotationId !== currentProps.annotationId || newProps.fieldName !== currentProps.fieldName;
|
||||
state.createGeometry =
|
||||
newProps.annotationId !== currentProps.annotationId
|
||||
|| newProps.fieldName !== currentProps.fieldName
|
||||
|| newProps.textFormat !== currentProps.textFormat
|
||||
|| !arrayEqual(newProps.groupByFields, currentProps.groupByFields);
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
@@ -50,16 +59,32 @@ export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<
|
||||
function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: MVSAnnotationLabelTextProps, text?: Text): Text {
|
||||
const { annotation, model } = getMVSAnnotationForStructure(structure, props.annotationId);
|
||||
const rows = annotation?.getRows() ?? [];
|
||||
const { count, offsets, grouped } = groupRows(rows);
|
||||
const builder = TextBuilder.create(props, count, count / 2, text);
|
||||
for (let iGroup = 0; iGroup < count; iGroup++) {
|
||||
const iFirstRowInGroup = grouped[offsets[iGroup]];
|
||||
const labelText = annotation!.getValueForRow(iFirstRowInGroup, props.fieldName);
|
||||
const groups = GroupedArray.groupIndices(rows, rowGroupingFunction(annotation!, props.groupByFields.map(x => x.fieldName)));
|
||||
const builder = TextBuilder.create(props, groups.count, groups.count / 2, text);
|
||||
const template = FormatTemplate(props.textFormat);
|
||||
for (let iGroup = 0; iGroup < groups.count; iGroup++) {
|
||||
const rowIndicesInGroup = GroupedArray.getGroup(groups, iGroup);
|
||||
const labelText = template.format(field => annotation!.getValueForRow(rowIndicesInGroup[0], field || props.fieldName));
|
||||
if (!labelText) continue;
|
||||
const rowsInGroup = grouped.slice(offsets[iGroup], offsets[iGroup + 1]).map(j => rows[j]);
|
||||
const rowsInGroup = rowIndicesInGroup.map(i => rows[i]);
|
||||
const p = textPropsForSelection(structure, rowsInGroup, model);
|
||||
if (!p) continue;
|
||||
builder.add(labelText, p.center[0], p.center[1], p.center[2], p.depth, p.scale, p.group);
|
||||
}
|
||||
return builder.getText();
|
||||
}
|
||||
|
||||
function rowGroupingFunction(annotation: MVSAnnotation, groupByFields: string[]): (row: MVSAnnotationRow, i: number) => string | undefined {
|
||||
if (groupByFields.length === 1) {
|
||||
const groupByField = groupByFields[0];
|
||||
return (row, i) => annotation.getValueForRow(i, groupByField);
|
||||
}
|
||||
if (groupByFields.length === 0) {
|
||||
return () => '';
|
||||
}
|
||||
return (row, i) => {
|
||||
const values = groupByFields.map(field => annotation.getValueForRow(i, field));
|
||||
if (values.includes(undefined)) return undefined;
|
||||
return values.join('\t');
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -45,11 +45,11 @@ export const MVSAnnotationStructureComponent = MVSTransform({
|
||||
to: SO.Molecule.Structure,
|
||||
params: MVSAnnotationStructureComponentParams,
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return createMVSAnnotationStructureComponent(a.data, params);
|
||||
apply({ a, params, cache }) {
|
||||
return createMVSAnnotationStructureComponent(a.data, params, cache as MVSComponentCache);
|
||||
},
|
||||
update: ({ a, b, oldParams, newParams }) => {
|
||||
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams);
|
||||
update: ({ a, b, oldParams, newParams, cache }) => {
|
||||
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams, cache as MVSComponentCache);
|
||||
},
|
||||
dispose({ b }) {
|
||||
b?.data.customPropertyDescriptors.dispose();
|
||||
@@ -75,8 +75,11 @@ export function createMVSAnnotationSubstructure(structure: Structure, params: MV
|
||||
}
|
||||
}
|
||||
|
||||
interface MVSComponentCache { source?: Structure }
|
||||
|
||||
/** Create a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps) {
|
||||
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
|
||||
cache.source = structure;
|
||||
const component = createMVSAnnotationSubstructure(structure, params);
|
||||
|
||||
if (params.nullIfEmpty && component.elementCount === 0) return StateObject.Null;
|
||||
@@ -102,15 +105,23 @@ export function createMVSAnnotationStructureComponent(structure: Structure, para
|
||||
}
|
||||
|
||||
/** Update a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps) {
|
||||
const change = !deepEqual(newParams, oldParams);
|
||||
const needsRecreate = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
|
||||
if (!change) {
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps, cache: MVSComponentCache) {
|
||||
const structureChanged = !cache.source || !Structure.areEquivalent(a, cache.source);
|
||||
cache.source = a;
|
||||
if (structureChanged) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
if (!needsRecreate) {
|
||||
|
||||
const coreParamsChanged = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
|
||||
if (coreParamsChanged) {
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
|
||||
const labelChanged = newParams.label !== oldParams.label;
|
||||
if (labelChanged) {
|
||||
b.label = newParams.label || b.label;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -11,6 +11,7 @@ import { Loci } from '../../../mol-model/loci';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { FormatTemplate } from '../../../mol-util/string-format';
|
||||
import { filterDefined } from '../helpers/utils';
|
||||
import { MVSAnnotationsProvider } from './annotation-prop';
|
||||
|
||||
@@ -21,6 +22,7 @@ export const MVSAnnotationTooltipsParams = {
|
||||
{
|
||||
annotationId: PD.Text('', { description: 'Reference to "MVS Annotation" custom model property' }),
|
||||
fieldName: PD.Text('tooltip', { description: 'Annotation field (column) from which to take color values' }),
|
||||
textFormat: PD.Text('{}', { description: 'Formatting template for tooltip text. Supports simplified f-string syntax. May reference multiple annotation fields. If value in any field is not defined, tooltip will not be displayed.' }),
|
||||
},
|
||||
obj => `${obj.annotationId}:${obj.fieldName}`
|
||||
),
|
||||
@@ -59,7 +61,9 @@ export const MVSAnnotationTooltipsLabelProvider = {
|
||||
const tooltipProps = MVSAnnotationTooltipsProvider.get(location.structure).value;
|
||||
if (!tooltipProps || tooltipProps.tooltips.length === 0) return undefined;
|
||||
const annotations = MVSAnnotationsProvider.get(location.unit.model).value;
|
||||
const texts = tooltipProps.tooltips.map(p => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, p.fieldName));
|
||||
const texts = tooltipProps.tooltips.map(p =>
|
||||
FormatTemplate(p.textFormat).format(field => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, field || p.fieldName))
|
||||
);
|
||||
return filterDefined(texts).join(' | ');
|
||||
default:
|
||||
return undefined;
|
||||
|
||||
@@ -61,6 +61,7 @@ export const ParseMVSX = MVSTransform({
|
||||
export const LoadMvsDataParams = {
|
||||
appendSnapshots: PD.Boolean(false, { description: 'If true, add snapshots from MVS into current snapshot list; if false, replace the snapshot list.' }),
|
||||
keepCamera: PD.Boolean(false, { description: 'If true, any camera positioning from the MVS state will be ignored and the current camera position will be kept.' }),
|
||||
keepCameraOrientation: PD.Boolean(false, { description: 'If true, any camera orientation from the MVS state will be ignored and the current camera orientation will be kept (camera target position will be loaded from MVS). keepCamera option overrides this.' }),
|
||||
applyExtensions: PD.Boolean(true, { description: 'If true, apply builtin MVS-loading extensions (not a part of standard MVS specification).' }),
|
||||
};
|
||||
|
||||
@@ -71,7 +72,7 @@ export const LoadMvsData = StateAction.build({
|
||||
params: LoadMvsDataParams,
|
||||
})(({ a, params }, plugin: PluginContext) => Task.create('Load MVS Data', async () => {
|
||||
const { mvsData, sourceUrl } = a.data;
|
||||
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
|
||||
await loadMVS(plugin, mvsData, { appendSnapshots: params.appendSnapshots, keepCamera: params.keepCamera, keepCameraOrientation: params.keepCameraOrientation, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
|
||||
}));
|
||||
|
||||
|
||||
@@ -112,11 +113,18 @@ export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any
|
||||
* add all contained files to `plugin`'s asset manager,
|
||||
* and parse the main file in the archive as MVSJ.
|
||||
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePathOrOptions?: string | { mainFilePath?: string, doNotClearAssets?: boolean }): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
// TODO: on next major version, streamline mainFilePathOrOptions
|
||||
if (typeof mainFilePathOrOptions === 'string') {
|
||||
mainFilePathOrOptions = { mainFilePath: mainFilePathOrOptions };
|
||||
}
|
||||
const mainFilePath = mainFilePathOrOptions?.mainFilePath ?? 'index.mvsj';
|
||||
const doNotClearAssets = mainFilePathOrOptions?.doNotClearAssets ?? false;
|
||||
|
||||
// Ensure at most one generation of MVSX file assets exists in the asset manager.
|
||||
// Hopefully, this is a reasonable compromise to ensure MVSX files work in multi-snapshot
|
||||
// states.
|
||||
clearMVSXFileAssets(plugin);
|
||||
if (!doNotClearAssets) clearMVSXFileAssets(plugin);
|
||||
|
||||
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
|
||||
let files: { [path: string]: Uint8Array<ArrayBuffer> };
|
||||
@@ -160,7 +168,7 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
|
||||
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
|
||||
}
|
||||
await plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>);
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, ...options, sourceUrl: parsed.sourceUrl });
|
||||
}));
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -72,7 +72,7 @@ export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { lay
|
||||
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
|
||||
* (the caller must ensure that any required custom properties be attached). */
|
||||
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
|
||||
const { colorLayers, granularity } = makeLayers(ctx, props, colorThemeRegistry);
|
||||
const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry);
|
||||
|
||||
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
|
||||
for (const layer of colorLayers) {
|
||||
@@ -101,7 +101,7 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
|
||||
return {
|
||||
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
|
||||
granularity,
|
||||
preferSmoothing: true,
|
||||
preferSmoothing,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Combines colors from multiple color themes.',
|
||||
@@ -136,6 +136,7 @@ interface ColorLayer {
|
||||
function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry) {
|
||||
const colorLayers: ColorLayer[] = [];
|
||||
let granularityFlags = 0;
|
||||
let preferSmoothing = false;
|
||||
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last
|
||||
const layer = props.layers[i];
|
||||
const themeProvider = colorThemeRegistry.get(layer.theme.name);
|
||||
@@ -175,8 +176,9 @@ function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, col
|
||||
default:
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
|
||||
}
|
||||
if (theme.preferSmoothing) preferSmoothing = true;
|
||||
}
|
||||
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags) };
|
||||
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing };
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ import { capitalize } from '../../../mol-util/string';
|
||||
import { rowsToExpression, rowToExpression } from '../helpers/selections';
|
||||
import { collectMVSReferences, decodeColor, isDefined } from '../helpers/utils';
|
||||
import { addParamDefaults } from '../tree/generic/params-schema';
|
||||
import { treeValidationIssues } from '../tree/generic/tree-validation';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode, MVSTreeSchema } from '../tree/mvs/mvs-tree';
|
||||
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
|
||||
@@ -66,6 +67,12 @@ export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives
|
||||
export class MVSPrimitivesData extends SO.Create<PrimitiveBuilderContext>({ name: 'Primitive Data', typeClass: 'Object' }) { }
|
||||
export class MVSPrimitiveShapes extends SO.Create<{ mesh?: Shape<Mesh>, labels?: Shape<Text> }>({ name: 'Primitive Shapes', typeClass: 'Object' }) { }
|
||||
|
||||
export interface MVSPrimitiveShapeSourceData {
|
||||
kind: 'mvs-primitives',
|
||||
node: MVSNode<'primitives'>,
|
||||
groupToNode: Map<number, MVSNode<'primitive'>>,
|
||||
}
|
||||
|
||||
export type MVSDownloadPrimitiveData = typeof MVSDownloadPrimitiveData
|
||||
export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
name: 'mvs-download-primitive-data',
|
||||
@@ -82,15 +89,35 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
const url = Asset.getUrlAsset(plugin.managers.asset, params.uri);
|
||||
const asset = await plugin.managers.asset.resolve(url, 'string').runInContext(ctx);
|
||||
const node = JSON.parse(StringLike.toString(asset.data)) as MolstarSubtree<'primitives'>;
|
||||
const validationIssues = treeValidationIssues(MVSTreeSchema, node, { anyRoot: true });
|
||||
if (validationIssues) {
|
||||
throw new Error(`Invalid primitive data from ${params.uri}:\n${validationIssues.join('\n')}`);
|
||||
}
|
||||
if (node.kind !== 'primitives') {
|
||||
throw new Error(`Expected primitives node from ${params.uri}, got ${node.kind}`);
|
||||
}
|
||||
const nodeWithDefaults: MolstarSubtree<'primitives'> = {
|
||||
...node,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, node.params || {}),
|
||||
children: node.children?.map((child: any) => {
|
||||
if (child.kind === 'primitive') {
|
||||
return {
|
||||
...child,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitive.params, child.params || {})
|
||||
};
|
||||
}
|
||||
return child;
|
||||
})
|
||||
};
|
||||
(cache as any).asset = asset;
|
||||
return new MVSPrimitivesData({
|
||||
node,
|
||||
node: nodeWithDefaults,
|
||||
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
|
||||
structureRefs: {},
|
||||
primitives: getPrimitives(node),
|
||||
options: { ...node.params },
|
||||
primitives: getPrimitives(nodeWithDefaults),
|
||||
options: { ...nodeWithDefaults.params },
|
||||
positionCache: new Map(),
|
||||
instances: getInstances(node.params),
|
||||
instances: getInstances(nodeWithDefaults.params),
|
||||
}, { label: 'Primitive Data' });
|
||||
});
|
||||
},
|
||||
@@ -584,7 +611,7 @@ function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shap
|
||||
kind: 'mvs-primitives',
|
||||
node: context.node,
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
} satisfies MVSPrimitiveShapeSourceData,
|
||||
MeshBuilder.getMesh(meshBuilder),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => 1,
|
||||
@@ -617,7 +644,7 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
|
||||
kind: 'mvs-primitives',
|
||||
node: context.node,
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
} satisfies MVSPrimitiveShapeSourceData,
|
||||
linesBuilder.getLines(),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => sizes.get(g) ?? 1,
|
||||
@@ -652,7 +679,7 @@ function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev: Text | und
|
||||
kind: 'mvs-primitives',
|
||||
node: context.node,
|
||||
groupToNode: state.groups.groupToNodeMap,
|
||||
},
|
||||
} satisfies MVSPrimitiveShapeSourceData,
|
||||
labelsBuilder.getText(),
|
||||
(g) => colors.get(g) as Color ?? color,
|
||||
(g) => sizes.get(g) ?? 1,
|
||||
|
||||
@@ -73,7 +73,7 @@ export const ElementSet = {
|
||||
/** Return a substructure of `structure` defined by `selector` */
|
||||
export function substructureFromSelector(structure: Structure, selector: Selector): Structure {
|
||||
const pso = (selector.name === 'annotation') ?
|
||||
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false })
|
||||
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false }, {})
|
||||
: createStructureComponent(structure, { type: selector, label: '', nullIfEmpty: false }, { source: structure });
|
||||
return PluginStateObject.Molecule.Structure.is(pso) ? pso.data : Structure.Empty;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { MVSTransform } from './annotation-structure-component';
|
||||
export const MVSTrajectoryWithCoordinates = MVSTransform({
|
||||
name: 'trajectory-with-coordinates',
|
||||
display: { name: 'Trajectory with Coordinates', description: 'Create a trajectory from existing model and the provided coordinates.' },
|
||||
from: PluginStateObject.Molecule.Model,
|
||||
from: [PluginStateObject.Molecule.Model, PluginStateObject.Molecule.Topology],
|
||||
to: PluginStateObject.Molecule.Trajectory,
|
||||
params: {
|
||||
coordinatesRef: ParamDefinition.Text('', { isHidden: true }),
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { range } from '../../../../mol-util/array';
|
||||
import { MVSAnnotationRow } from '../schemas';
|
||||
import { groupRows } from '../selections';
|
||||
import { GroupedArray } from '../utils';
|
||||
|
||||
|
||||
describe('groupRows', () => {
|
||||
it('groupRows', async () => {
|
||||
describe('GroupedArray', () => {
|
||||
it('GroupedArray.groupIndices', async () => {
|
||||
const rows = [
|
||||
{ label: 'A' }, { label: 'B', group_id: 1 }, { label: 'C', group_id: 'x' }, { label: 'D', group_id: 1 },
|
||||
{ label: 'E' }, { label: 'F' }, { label: 'G', group_id: 'x' }, { label: 'H', group_id: 'x' },
|
||||
] as any as MVSAnnotationRow[];
|
||||
const g = groupRows(rows);
|
||||
const g = GroupedArray.groupIndices(rows, row => row.group_id);
|
||||
const groupedIndices = range(g.count).map(i => g.grouped.slice(g.offsets[i], g.offsets[i + 1]));
|
||||
const groupedRows = groupedIndices.map(group => group.map(j => rows[j]));
|
||||
expect(groupedRows).toEqual([
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -208,6 +208,7 @@ function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: Ato
|
||||
}
|
||||
arrayExtend(result, residuesHere);
|
||||
}
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -419,7 +420,7 @@ function getQualifyingCoarseElements(coarseElements: CoarseElements, row: MVSAnn
|
||||
// This implementation can yield some elements even when queryStart>queryEnd (e.g. { beg_label_seq_id: 70, end_label_seq_id: 58, label_seq_id: 60 } -> sphere 51-100 qualifies ).
|
||||
// This is on purpose, to have the same behavior as MolScript.
|
||||
}
|
||||
sortIfNeeded(result, iElem => iElem);
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -446,43 +447,3 @@ export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression
|
||||
items: rows as StructureElement.SchemaItem[]
|
||||
});
|
||||
}
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
/** Return row indices grouped by `row.group_id`. Rows with `row.group_id===undefined` are treated as separate groups. */
|
||||
export function groupRows(rows: readonly MVSAnnotationRow[]): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const group_id = rows[i].group_id;
|
||||
if (!isDefined(group_id)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(group_id);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(group_id, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowIndices = range(rows.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (i === 0 || groups[rowIndices[i]] !== groups[rowIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(rowIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: rowIndices };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -7,8 +7,8 @@
|
||||
|
||||
import { hashString } from '../../../mol-data/util';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { decodeColor as _decodeColor } from '../../../mol-util/color/utils';
|
||||
|
||||
|
||||
@@ -107,29 +107,6 @@ export function decodeColor(colorString: string | number | undefined | null): Co
|
||||
return _decodeColor(colorString);
|
||||
}
|
||||
|
||||
/** Regular expression matching a hexadecimal color string, e.g. '#FF1100' or '#f10' */
|
||||
const hexColorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
|
||||
/** Hexadecimal color string, e.g. '#FF1100' (the type matches more than just valid HexColor strings) */
|
||||
export type HexColor = `#${string}`
|
||||
|
||||
export const HexColor = {
|
||||
/** Decide if a string is a valid hexadecimal color string (6-digit or 3-digit, e.g. '#FF1100' or '#f10') */
|
||||
is(str: any): str is HexColor {
|
||||
return typeof str === 'string' && hexColorRegex.test(str);
|
||||
},
|
||||
};
|
||||
|
||||
/** Named color string, e.g. 'red' */
|
||||
export type ColorName = keyof ColorNames
|
||||
|
||||
export const ColorName = {
|
||||
/** Decide if a string is a valid named color string */
|
||||
is(str: any): str is ColorName {
|
||||
return str in ColorNames;
|
||||
},
|
||||
};
|
||||
|
||||
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
|
||||
const ret: any = {};
|
||||
|
||||
@@ -173,4 +150,50 @@ export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dep
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
export interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
export const GroupedArray = {
|
||||
getGroup<T>(groupedArray: GroupedArray<T>, iGroup: number): T[] {
|
||||
return groupedArray.grouped.slice(groupedArray.offsets[iGroup], groupedArray.offsets[iGroup + 1]);
|
||||
},
|
||||
/** Return element indices grouped by `group_by(element, index)`. Elements with `group_by(element, index)===undefined` are treated as separate groups. */
|
||||
groupIndices<T>(elements: readonly T[], group_by: (element: T, index: number) => string | undefined): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
const groupId = group_by(elements[i], i);
|
||||
if (!isDefined(groupId)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(groupId);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(groupId, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const elementIndices = range(elements.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
if (i === 0 || groups[elementIndices[i]] !== groups[elementIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(elementIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: elementIndices };
|
||||
},
|
||||
};
|
||||
|
||||
8
src/extensions/mvs/index.ts
Normal file
8
src/extensions/mvs/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export * from './mvs-data';
|
||||
export * from './load';
|
||||
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { VolumeStreaming } from '../../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
|
||||
import { CreateVolumeStreamingBehavior, CreateVolumeStreamingInfo, VolumeStreamingVisual } from '../../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
|
||||
import { mapObjectMap } from '../../../mol-util/object';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
import { MolstarLoadingExtension } from '../load';
|
||||
import { UpdateTarget } from '../load-generic';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
|
||||
/** Type of `molstar_volume_streaming` custom property, used by `VolumeStreamingExtension` MVS loading extension. */
|
||||
export type MolstarVolumeStreamingCustomProp = {
|
||||
/** URL of the volume streaming server, e.g. 'https://www.ebi.ac.uk/pdbe/densities'. */
|
||||
server_url?: string,
|
||||
/** Volume streaming view type ('off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'). Default value depends on structure type (X-ray/EM). */
|
||||
view?: VolumeStreaming.ViewTypes,
|
||||
/** Customization of channel parameters. */
|
||||
channel_params?: { [name in VolumeStreaming.ChannelType]?: Partial<ChannelParams_> },
|
||||
/** List of volume streaming entries (if not specified, will be retrieved automatically based on PDB ID) */
|
||||
entries?: ReturnType<typeof CreateVolumeStreamingInfo['createDefaultParams']>['entries'],
|
||||
} | boolean | undefined;
|
||||
|
||||
|
||||
/** This MVS loading extension allows turning on volume streaming for a structure by providing custom property `molstar_volume_streaming`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1cbs_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: true,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1tqn_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* channel_params: {
|
||||
* '2fo-fc': { color: 'skyblue', opacity: 0.3 },
|
||||
* 'fo-fc(+ve)': { color: 'greenyellow', wireframe: true, isoValue: { kind: 'relative', relativeValue: +2.5 } },
|
||||
* 'fo-fc(-ve)': { color: 'orange', wireframe: true, isoValue: { kind: 'relative', relativeValue: -2.5 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/8hra_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* server_url: 'https://www.ebi.ac.uk/pdbe/densities', // = default
|
||||
* entries: [{ dataId: 'EMD-34965', source: { name: 'em', params: { isoValue: { kind: 'absolute', absoluteValue: 0.015 } } } }],
|
||||
* view: 'auto', // default is 'auto' for EM, 'selection-box' for X-ray structures
|
||||
* channel_params: {
|
||||
* em: { color: '#ff0000', opacity: 0.4, isoValue: { kind: 'absolute', absoluteValue: 0.025 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
* ```
|
||||
*/
|
||||
export const VolumeStreamingExtension: MolstarLoadingExtension<{}> = {
|
||||
id: 'wwpdb/volume-streaming',
|
||||
description: 'Allow turning on volume streaming for a structure',
|
||||
createExtensionContext: () => ({}),
|
||||
action: (updateTarget, node, context, extContext) => {
|
||||
if (node.kind !== 'structure') return;
|
||||
let params: MolstarVolumeStreamingCustomProp = node.custom?.molstar_volume_streaming;
|
||||
if (!params) return;
|
||||
if (params === true) params = {};
|
||||
|
||||
const streamingInfo = UpdateTarget.apply(updateTarget, CreateVolumeStreamingInfo, {
|
||||
serverUrl: params.server_url,
|
||||
autoEntries: !params.entries,
|
||||
entries: params.entries,
|
||||
defaultView: params.view,
|
||||
defaultChannelParams: params.channel_params && mapObjectMap(params.channel_params, normalizeChannelParams),
|
||||
}, { state: { isCollapsed: true } });
|
||||
|
||||
const streamingBehavior = UpdateTarget.apply(streamingInfo, CreateVolumeStreamingBehavior);
|
||||
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true }, tags: '2fo-fc' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true }, tags: 'fo-fc(+ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true }, tags: 'fo-fc(-ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true }, tags: 'em' });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
interface ChannelParams_ extends Omit<VolumeStreaming.ChannelParams, 'color'> {
|
||||
color: ColorT | number,
|
||||
}
|
||||
|
||||
function normalizeChannelParams(p: Partial<ChannelParams_> | undefined): Partial<VolumeStreaming.ChannelParams> | undefined {
|
||||
if (!p) return undefined;
|
||||
return {
|
||||
...p,
|
||||
color: decodeColor(p.color),
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -19,7 +19,7 @@ import { ColorListEntry } from '../../mol-util/color/color';
|
||||
import { canonicalJsonString } from '../../mol-util/json';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { MVSAnnotationColorThemeProps, MVSAnnotationColorThemeProvider, MVSCategoricalPaletteProps, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationLabelProps, MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationSpec } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponentProps } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProps } from './components/annotation-tooltips-prop';
|
||||
@@ -196,7 +196,7 @@ export function collectAnnotationTooltips(tree: MolstarSubtree<'structure'>, con
|
||||
if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
if (annotationId) {
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name });
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name, textFormat: node.params.text_format });
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -338,11 +338,13 @@ export function prettyNameFromSelector(selector?: MolstarNodeParams<'component'>
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
|
||||
export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from_source'>, context: MolstarLoadingContext): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
const annotationId = context.annotationMap.get(node)!;
|
||||
const fieldName = node.params.field_name;
|
||||
const textFormat = node.params.text_format;
|
||||
const groupBy = node.params.group_by_fields ?? [node.params.field_remapping.group_id ?? 'group_id'];
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
return {
|
||||
type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName } },
|
||||
type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName, textFormat, groupByFields: groupBy.map(x => ({ fieldName: x })) } satisfies Partial<MVSAnnotationLabelProps> },
|
||||
colorTheme: colorThemeForNode(nearestReprNode, context),
|
||||
};
|
||||
}
|
||||
@@ -376,7 +378,8 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'line':
|
||||
return {
|
||||
@@ -390,7 +393,8 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
};
|
||||
case 'carbohydrate':
|
||||
return {
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: 1.75 } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'surface': {
|
||||
return {
|
||||
@@ -401,6 +405,15 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
case 'putty': {
|
||||
const sizeTheme = params.size_theme ?? 'uniform';
|
||||
return {
|
||||
type: { name: 'putty', params: { alpha, sizeFactor: params.size_factor } },
|
||||
sizeTheme: sizeTheme === 'uncertainty'
|
||||
? { name: 'uncertainty', params: {} }
|
||||
: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
@@ -520,7 +533,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
c => {
|
||||
const theme = colorThemeForNode(c, context);
|
||||
if (!theme) return undefined;
|
||||
return { theme, selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) };
|
||||
return { theme, selection: componentPropsFromSelector(c.params.selector) };
|
||||
}
|
||||
).filter(t => !!t);
|
||||
return {
|
||||
@@ -555,11 +568,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
}
|
||||
|
||||
function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>): boolean {
|
||||
if (node.kind === 'color') {
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
}
|
||||
|
||||
const FALLBACK_COLOR = decodeColor(DefaultColor)!;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -8,8 +8,8 @@
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Download, ParseCcp4, ParseCif, ParseDx } from '../../mol-plugin-state/transforms/data';
|
||||
import { CoordinatesFromLammpstraj, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
|
||||
import { Download, ParseCcp4, ParseCif, ParseDx, ParsePrmtop, ParsePsf, ParseTop } from '../../mol-plugin-state/transforms/data';
|
||||
import { CoordinatesFromDcd, CoordinatesFromLammpstraj, CoordinatesFromNctraj, CoordinatesFromTrr, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TopologyFromPrmtop, TopologyFromPsf, TopologyFromTop, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif, VolumeFromDx } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
@@ -31,6 +31,7 @@ import { MVSTrajectoryWithCoordinates } from './components/trajectory';
|
||||
import { generateStateTransition } from './helpers/animation';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { VolumeStreamingExtension } from './load-extensions/volume-streaming';
|
||||
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
|
||||
@@ -46,6 +47,8 @@ export interface MVSLoadOptions {
|
||||
appendSnapshots?: boolean,
|
||||
/** Ignore any camera positioning from the MVS state and keep the current camera position instead, ignore any camera positioning when generating snapshots. */
|
||||
keepCamera?: boolean,
|
||||
/** Follow camera target position from the MVS state but keep the current camera direction and up. (`keepCamera` option overrides this) */
|
||||
keepCameraOrientation?: boolean,
|
||||
/** Specifies a set of MVS-loading extensions (not a part of standard MVS specification). If undefined, apply all builtin extensions. If `[]`, do not apply builtin extensions. */
|
||||
extensions?: MolstarLoadingExtension<any>[],
|
||||
/** Run some sanity checks and print potential issues to the console. */
|
||||
@@ -95,7 +98,10 @@ async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSDat
|
||||
options
|
||||
);
|
||||
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
|
||||
entries.push(entry);
|
||||
entries.push({
|
||||
...entry,
|
||||
_transientData: { sourceMvsSnapshot: snapshot }
|
||||
});
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });
|
||||
@@ -172,17 +178,24 @@ function molstarTreeToEntry(
|
||||
tree: MolstarTree,
|
||||
animation: MVSAnimationNode<'animation'> | undefined,
|
||||
metadata: SnapshotMetadata & { previousTransitionDurationMs?: number },
|
||||
options: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
options: { keepCamera?: boolean, keepCameraOrientation?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined,
|
||||
};
|
||||
if (!options?.keepCamera) {
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
if (options?.keepCamera) {
|
||||
// do nothing
|
||||
} else if (options.keepCameraOrientation) {
|
||||
// load camera target, keep orientation
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs, ignoreCameraOrientation: true });
|
||||
} else {
|
||||
// fully load camera
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs });
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
snapshot.structureFocus = {}; // avoid structure focus persisting through states (causes weird behaviors, e.g. when turning on Volume Streaming)
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
@@ -246,7 +259,16 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
case 'mol2':
|
||||
case 'xtc':
|
||||
case 'lammpstrj':
|
||||
case 'dcd':
|
||||
case 'nctraj':
|
||||
case 'trr':
|
||||
return updateParent;
|
||||
case 'psf':
|
||||
return UpdateTarget.apply(updateParent, ParsePsf, {});
|
||||
case 'prmtop':
|
||||
return UpdateTarget.apply(updateParent, ParsePrmtop, {});
|
||||
case 'top':
|
||||
return UpdateTarget.apply(updateParent, ParseTop, {});
|
||||
case 'map':
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
case 'dx':
|
||||
@@ -260,6 +282,12 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
coordinates(updateParent: UpdateTarget, node: MolstarNode<'coordinates'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'nctraj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromNctraj);
|
||||
case 'dcd':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromDcd);
|
||||
case 'trr':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromTrr);
|
||||
case 'xtc':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromXtc);
|
||||
case 'lammpstrj':
|
||||
@@ -279,7 +307,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { variant: format });
|
||||
case 'gro':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
|
||||
case 'xyz':
|
||||
@@ -303,6 +331,28 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
topology_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'topology_with_coordinates'>): UpdateTarget | undefined {
|
||||
let parsed: UpdateTarget;
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'psf':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPsf, {});
|
||||
break;
|
||||
case 'prmtop':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPrmtop, {});
|
||||
break;
|
||||
case 'top':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromTop, {});
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown format in "topology_with_coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
const result = UpdateTarget.apply(parsed, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const annotations = collectAnnotationReferences(node, context);
|
||||
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
|
||||
@@ -427,12 +477,15 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
const refs = getPrimitiveStructureRefs(tree);
|
||||
const clip = clippingForNode(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
|
||||
const refs = new Set(tree.params.references);
|
||||
const clip = clippingForNode(tree);
|
||||
return applyPrimitiveVisuals(data, new Set(tree.params.references), clip);
|
||||
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -451,4 +504,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
|
||||
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
|
||||
NonCovalentInteractionsExtension,
|
||||
IsHiddenCustomStateExtension,
|
||||
VolumeStreamingExtension,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { ajaxGet } from '../../mol-util/data-source';
|
||||
import { deepClone } from '../../mol-util/object';
|
||||
import { createMVSX } from './export';
|
||||
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
|
||||
import { findUris, replaceUris, resolveUri, treeToString, windowUrl } from './tree/generic/tree-utils';
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -102,6 +105,55 @@ export const MVSData = {
|
||||
return JSON.stringify(mvsData, undefined, space);
|
||||
},
|
||||
|
||||
/** Encode `MVSData` to MVSX (MolViewSpec JSON zipped together with referenced assets). Automatically fetches all referenced assets unless specified otherwise in `options`. */
|
||||
async toMVSX(mvsData: MVSData, options: {
|
||||
/** Explicitely define assets to be included in the MVSX (binary data or string with asset content).
|
||||
* If not specified, assets will be fetched automatically. */
|
||||
assets?: { [uri: string]: Uint8Array<ArrayBuffer> | string },
|
||||
/** Base URI for resolving relative URIs (only applies if `assets` not specified). */
|
||||
baseUri?: string,
|
||||
/** Do not include external resources (i.e. absolute URIs) in the MVSX (default is to include both relative and absolute URIs) (only applies if `assets` not specified). */
|
||||
skipExternal?: boolean,
|
||||
/** Optional cache for sharing fetched assets across multiple `toMVSX` calls (only applies if `assets` not specified). */
|
||||
cache?: { [absoluteUri: string]: Uint8Array<ArrayBuffer> | string },
|
||||
} = {}): Promise<Uint8Array<ArrayBuffer>> {
|
||||
let { assets, baseUri, skipExternal, cache } = options;
|
||||
mvsData = deepClone(mvsData);
|
||||
const uriParamNames = ['uri', 'url'];
|
||||
const trees = mvsData.kind === 'multiple' ? mvsData.snapshots.map(s => s.root) : [mvsData.root];
|
||||
// Fetch assets:
|
||||
if (!assets) {
|
||||
assets = {};
|
||||
cache ??= {};
|
||||
const theWindowUrl = windowUrl();
|
||||
const uris = new Set<string>();
|
||||
for (const tree of trees) {
|
||||
findUris(tree, uriParamNames, uris);
|
||||
}
|
||||
for (const uri of uris) {
|
||||
if (skipExternal && isAbsoluteUri(uri)) continue;
|
||||
const resolvedUri = resolveUri(uri, baseUri, theWindowUrl)!;
|
||||
const content = cache[resolvedUri] ??= await ajaxGet({ url: resolvedUri, type: 'binary' }).run();
|
||||
assets[uri] = content;
|
||||
}
|
||||
}
|
||||
// Replace URIs by asset names:
|
||||
const uriMapping: Record<string, string> = {};
|
||||
const namedAssets: { name: string, content: string | Uint8Array<ArrayBuffer> }[] = [];
|
||||
let counter = 0;
|
||||
for (const uri in assets) {
|
||||
const nameHint = uri.split('/').pop()!.replace(/[^\w\.+-]/g, '_').slice(0, 64);
|
||||
const assetName = `./assets/${counter++}-${nameHint}`;
|
||||
uriMapping[uri] = assetName;
|
||||
namedAssets.push({ name: assetName, content: assets[uri] });
|
||||
}
|
||||
for (const tree of trees) {
|
||||
replaceUris(tree, uriMapping, uriParamNames);
|
||||
}
|
||||
// Zip:
|
||||
return await createMVSX(mvsData, namedAssets);
|
||||
},
|
||||
|
||||
/** Validate `MVSData`. Return `true` if OK; `false` if not OK.
|
||||
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
|
||||
isValid(mvsData: MVSData, options: { noExtra?: boolean } = {}): boolean {
|
||||
@@ -207,3 +259,12 @@ function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: {
|
||||
function utcNowISO(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function isAbsoluteUri(uri: string): boolean {
|
||||
try {
|
||||
const url = new URL(uri);
|
||||
return !!url.protocol;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user