mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 07:04:22 +08:00
Compare commits
424 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd872b47e6 | ||
|
|
2683c5b318 | ||
|
|
c71f60a164 | ||
|
|
881cbc1947 | ||
|
|
f3e7febbd1 | ||
|
|
e68ad13031 | ||
|
|
7fbbe1e63a | ||
|
|
a5ca72af3c | ||
|
|
1ce6641eb3 | ||
|
|
5dc413ab8c | ||
|
|
50b615e86c | ||
|
|
5b4c6743e7 | ||
|
|
99a3906978 | ||
|
|
981db34736 | ||
|
|
c079a8c5a8 | ||
|
|
ad70adf6ce | ||
|
|
89110b52bd | ||
|
|
8a69f050a6 | ||
|
|
9e38a44406 | ||
|
|
3514ab23c3 | ||
|
|
b59e3c383d | ||
|
|
eeba565d78 | ||
|
|
687e54cc87 | ||
|
|
ac73939440 | ||
|
|
7a3eb8d03f | ||
|
|
3d26904e0b | ||
|
|
468e14bc35 | ||
|
|
e2dc61212e | ||
|
|
aa911ad4bc | ||
|
|
bb5494264c | ||
|
|
c0116a3baa | ||
|
|
9c7497b447 | ||
|
|
fa3a79fdeb | ||
|
|
2987240df4 | ||
|
|
17a1640da5 | ||
|
|
a86da8ee11 | ||
|
|
20e373115d | ||
|
|
531260fbc5 | ||
|
|
47a3dfcef9 | ||
|
|
c5ca51fd80 | ||
|
|
2f2e44c032 | ||
|
|
26acb37098 | ||
|
|
466308cde8 | ||
|
|
dc9af9d8b0 | ||
|
|
c17bfd65e7 | ||
|
|
6de07ab8c2 | ||
|
|
0b8aab802c | ||
|
|
13f28fbe33 | ||
|
|
2eda679966 | ||
|
|
35c778b644 | ||
|
|
96f8ba5a80 | ||
|
|
0daaa94958 | ||
|
|
e672503fda | ||
|
|
4d86c9e0ae | ||
|
|
80fbc474f6 | ||
|
|
dacdc6abfc | ||
|
|
58bc8b58de | ||
|
|
07ead670dd | ||
|
|
00fd760f71 | ||
|
|
a71186905d | ||
|
|
a7e0524d01 | ||
|
|
7d7c1241d4 | ||
|
|
1f6e928d78 | ||
|
|
9bc256bdab | ||
|
|
734096260d | ||
|
|
1b4b6f9435 | ||
|
|
54fe5c85d6 | ||
|
|
f336891bf3 | ||
|
|
d2a3c9c61f | ||
|
|
6968959fe2 | ||
|
|
7749fe5000 | ||
|
|
bf45d2df5d | ||
|
|
b2222844ae | ||
|
|
3d21f1ecc6 | ||
|
|
cde280de60 | ||
|
|
9b415ddff2 | ||
|
|
906c3ac2b6 | ||
|
|
498611d4d4 | ||
|
|
a11bc73d68 | ||
|
|
9616ae5d63 | ||
|
|
c81476d2a7 | ||
|
|
397f001352 | ||
|
|
7edf274477 | ||
|
|
3c1a26c4f5 | ||
|
|
1c695846d5 | ||
|
|
a4c6d1e0e6 | ||
|
|
e51fe83800 | ||
|
|
316076d81e | ||
|
|
4073055d8d | ||
|
|
c6e0ec1c06 | ||
|
|
49aaa48e6e | ||
|
|
0eb882883e | ||
|
|
a6c25551dd | ||
|
|
0a3f73860a | ||
|
|
1de159d65c | ||
|
|
e2c411fefe | ||
|
|
3cf1c64e12 | ||
|
|
b159752b72 | ||
|
|
0d7db59c9e | ||
|
|
a8bf90a68b | ||
|
|
96aff39272 | ||
|
|
a9ae08fc1f | ||
|
|
a24f989c01 | ||
|
|
41ff45d14c | ||
|
|
6ad80bf66b | ||
|
|
eeed48a1f7 | ||
|
|
232bc0d076 | ||
|
|
ac6b87add4 | ||
|
|
2e3bff7d48 | ||
|
|
bd223b4c39 | ||
|
|
a75dc11427 | ||
|
|
30acaffb72 | ||
|
|
2818102b8b | ||
|
|
519e5a6f92 | ||
|
|
071740e7c1 | ||
|
|
7aafb2f4c3 | ||
|
|
3c72988d77 | ||
|
|
3c01dfbd42 | ||
|
|
3a5829aa3e | ||
|
|
ffeeddb37a | ||
|
|
50945493c1 | ||
|
|
fa80c4797a | ||
|
|
650e8bf703 | ||
|
|
13d57737ae | ||
|
|
a6d1a3dfdd | ||
|
|
afffdc06e5 | ||
|
|
80f1b1c795 | ||
|
|
06111e2731 | ||
|
|
adb49371bb | ||
|
|
7b726ded20 | ||
|
|
9f85a0c840 | ||
|
|
f92755c920 | ||
|
|
d141c27765 | ||
|
|
062ac65f0f | ||
|
|
bb420d0806 | ||
|
|
0018032423 | ||
|
|
3dd48ac73c | ||
|
|
4632a6f305 | ||
|
|
eda570d4f1 | ||
|
|
b0127d746d | ||
|
|
5a66ca69c4 | ||
|
|
1c17277f03 | ||
|
|
d771bdc8ff | ||
|
|
eace3f4259 | ||
|
|
8f88da70a6 | ||
|
|
b797be9642 | ||
|
|
2395b7a10a | ||
|
|
0764795c08 | ||
|
|
ea8b7a1d56 | ||
|
|
7c6827f5f5 | ||
|
|
5a6f16ef8d | ||
|
|
8dfdcdd0b7 | ||
|
|
67a2594108 | ||
|
|
871f9635e3 | ||
|
|
25251f3546 | ||
|
|
98824f477e | ||
|
|
aae9a117e8 | ||
|
|
452639c3ce | ||
|
|
6bd45e0a9b | ||
|
|
be100a3ac6 | ||
|
|
96a8cd789c | ||
|
|
d195e1dbf5 | ||
|
|
e6a8e788f5 | ||
|
|
a755ed441e | ||
|
|
de8f294329 | ||
|
|
021171c07d | ||
|
|
013ddb72ed | ||
|
|
207c226f66 | ||
|
|
b4ff98499b | ||
|
|
8471d337a2 | ||
|
|
2f84b94227 | ||
|
|
8dcd6063b7 | ||
|
|
caefe7ba67 | ||
|
|
ad6cebc59b | ||
|
|
e8d2e6d806 | ||
|
|
39352c40d1 | ||
|
|
9994262abc | ||
|
|
5bcf923381 | ||
|
|
ab5dd0b733 | ||
|
|
5a8a6310f8 | ||
|
|
a634c7a587 | ||
|
|
353c5d6d95 | ||
|
|
92698c486c | ||
|
|
898dd1161d | ||
|
|
361f289d0e | ||
|
|
b49d036fcd | ||
|
|
cfee9d86c0 | ||
|
|
92622dfbd7 | ||
|
|
80c2876350 | ||
|
|
fb99f6db8a | ||
|
|
862f8193ef | ||
|
|
490c6679eb | ||
|
|
dd278ca964 | ||
|
|
0892bb24d0 | ||
|
|
83ce5e9422 | ||
|
|
c4ba92c7cb | ||
|
|
846bdf10b0 | ||
|
|
91a46ea7df | ||
|
|
e4ec68a86c | ||
|
|
410655052f | ||
|
|
17162e967a | ||
|
|
c119a1bc21 | ||
|
|
7f2e98f714 | ||
|
|
82f5d8be21 | ||
|
|
73fa675346 | ||
|
|
1f2812b2e3 | ||
|
|
6f05179db8 | ||
|
|
fc8848e97c | ||
|
|
b991102bfa | ||
|
|
e3768805a6 | ||
|
|
cdb65665a6 | ||
|
|
3765bc410c | ||
|
|
69bbd76f33 | ||
|
|
b61b3e1115 | ||
|
|
835f717e47 | ||
|
|
6a35a3ece0 | ||
|
|
518621a1bd | ||
|
|
51acfa1dce | ||
|
|
5be6c9176a | ||
|
|
dfa83c94f7 | ||
|
|
67aedd4770 | ||
|
|
346eb59da9 | ||
|
|
3195594ef3 | ||
|
|
489b412308 | ||
|
|
2d0e8d4ca0 | ||
|
|
27f94c81a2 | ||
|
|
1e865ecacc | ||
|
|
f293a02485 | ||
|
|
ddf00600c6 | ||
|
|
88cd639493 | ||
|
|
0a30ed45f9 | ||
|
|
b5b282c141 | ||
|
|
c4c60cb263 | ||
|
|
e3cf4e928e | ||
|
|
d8a08ef900 | ||
|
|
8b8f3bf492 | ||
|
|
3983073d6c | ||
|
|
82b22fa3f2 | ||
|
|
8a38f73771 | ||
|
|
37da82b138 | ||
|
|
dd17cb23d9 | ||
|
|
8f3afd9f7c | ||
|
|
6e39188f0b | ||
|
|
667cacea12 | ||
|
|
49f0ec981c | ||
|
|
a60d6e9223 | ||
|
|
2d111c1e25 | ||
|
|
874cde4f72 | ||
|
|
826318760e | ||
|
|
de790051aa | ||
|
|
00bf75839e | ||
|
|
b9d4501dcc | ||
|
|
46fb1789b0 | ||
|
|
a1e8bf841b | ||
|
|
6662dbfdd6 | ||
|
|
39b9710d16 | ||
|
|
4fe303da72 | ||
|
|
0662506d35 | ||
|
|
ea987f5601 | ||
|
|
bcae586122 | ||
|
|
e56f188a12 | ||
|
|
8fda9beb7b | ||
|
|
0f50a6682b | ||
|
|
524d38c450 | ||
|
|
68a2e52355 | ||
|
|
447d1f940f | ||
|
|
1cbb59b5d0 | ||
|
|
6f5bcdef90 | ||
|
|
f6f1c5a350 | ||
|
|
f6545c38be | ||
|
|
53fe73d3ee | ||
|
|
3bf5ab1ef7 | ||
|
|
b4813ff866 | ||
|
|
845269e9a5 | ||
|
|
59968d92ab | ||
|
|
d5b7cd370b | ||
|
|
b4434cce17 | ||
|
|
e73227519b | ||
|
|
f8e6d5cbfb | ||
|
|
70bde8c899 | ||
|
|
361dce2b96 | ||
|
|
c83ce28bf4 | ||
|
|
b9a3620a4c | ||
|
|
a939a57811 | ||
|
|
8388ee8f1e | ||
|
|
befa40f5a2 | ||
|
|
36257e2b0f | ||
|
|
769022e88c | ||
|
|
51c180a8f4 | ||
|
|
2ffc5dc5c0 | ||
|
|
431ba01117 | ||
|
|
c6a4350b81 | ||
|
|
9ef8d0c9f8 | ||
|
|
e7606477c2 | ||
|
|
de093b5472 | ||
|
|
88cd9184d8 | ||
|
|
055c169c1f | ||
|
|
1988275695 | ||
|
|
82451bff00 | ||
|
|
e6fd0202a6 | ||
|
|
c21d84dd62 | ||
|
|
f392ac21cd | ||
|
|
d4e5473b86 | ||
|
|
760298c6bf | ||
|
|
d949b99629 | ||
|
|
fff9719e48 | ||
|
|
5858a6eb19 | ||
|
|
46acc1b95e | ||
|
|
da4654b859 | ||
|
|
32869a9a45 | ||
|
|
4cfbccc5d6 | ||
|
|
6301196e67 | ||
|
|
0c61c2badd | ||
|
|
1207526161 | ||
|
|
405d9d524f | ||
|
|
f724717821 | ||
|
|
6247efa8b6 | ||
|
|
7045545419 | ||
|
|
17a0a2be6f | ||
|
|
0e92cfa007 | ||
|
|
04e17872d0 | ||
|
|
59255c720d | ||
|
|
eb71e2c606 | ||
|
|
db2905ba9f | ||
|
|
d097a4abd2 | ||
|
|
459cfd7ab8 | ||
|
|
317229afee | ||
|
|
1e2f16d6b3 | ||
|
|
be07c1668f | ||
|
|
90ddb3dc34 | ||
|
|
f968e86387 | ||
|
|
56639f0bda | ||
|
|
8ae40bfd7c | ||
|
|
a7901c53ce | ||
|
|
76f856fa4f | ||
|
|
63a4cda442 | ||
|
|
603aa89609 | ||
|
|
51dd388912 | ||
|
|
c4708f0260 | ||
|
|
dcfe2e3072 | ||
|
|
9d536fefff | ||
|
|
ed69d15ee1 | ||
|
|
4cd7f0575e | ||
|
|
ad1507dadf | ||
|
|
c358259437 | ||
|
|
6b961c532f | ||
|
|
bb8f872a13 | ||
|
|
93df8a65cd | ||
|
|
de9fd2fcd7 | ||
|
|
24ae8dfda8 | ||
|
|
909e4b3a9f | ||
|
|
09c46447d9 | ||
|
|
239a7cc072 | ||
|
|
f7ccff61e0 | ||
|
|
f8a7483467 | ||
|
|
0a7b3fa396 | ||
|
|
e5cf8bcc04 | ||
|
|
f93230ce44 | ||
|
|
bdde2cea31 | ||
|
|
d44bb6c908 | ||
|
|
052e30b739 | ||
|
|
48a7ac80f5 | ||
|
|
c9717c2332 | ||
|
|
aaed0a9a63 | ||
|
|
c29648ac9b | ||
|
|
cb1f52a8f4 | ||
|
|
5f759edc1b | ||
|
|
8897261836 | ||
|
|
23b1426567 | ||
|
|
329658ff54 | ||
|
|
482059cc9b | ||
|
|
4e509fc479 | ||
|
|
6b1edd9d10 | ||
|
|
d0c692fe03 | ||
|
|
36ee1089a8 | ||
|
|
723bf3e657 | ||
|
|
dc08e524b3 | ||
|
|
3ecd305adf | ||
|
|
60eb42391e | ||
|
|
26f5210518 | ||
|
|
7b41d800c0 | ||
|
|
3aa9ef7595 | ||
|
|
fad5a40ec4 | ||
|
|
58e0ba016e | ||
|
|
b8f168ebf5 | ||
|
|
560c26c73f | ||
|
|
73ada6b1f1 | ||
|
|
4f2cee3b56 | ||
|
|
ad6975c99d | ||
|
|
bea462b8b2 | ||
|
|
5652fa55db | ||
|
|
d2208a0814 | ||
|
|
62a456ce82 | ||
|
|
bef1142a31 | ||
|
|
8e07de62dc | ||
|
|
347b9ead6b | ||
|
|
6d9a09620c | ||
|
|
17349b8529 | ||
|
|
3a200fe2d7 | ||
|
|
10bd513680 | ||
|
|
9214c54e7d | ||
|
|
64da492d63 | ||
|
|
cbb65aaaac | ||
|
|
78c9dda257 | ||
|
|
20450e352f | ||
|
|
9559c22858 | ||
|
|
0d39dc69f1 | ||
|
|
1a29159dfd | ||
|
|
8c21d3b9d9 | ||
|
|
24143d7078 | ||
|
|
22ead527f2 | ||
|
|
104666a13e | ||
|
|
f9ea48fd7b | ||
|
|
80598dc102 | ||
|
|
fbaaa57ca2 | ||
|
|
b0113d6189 | ||
|
|
da1fa03a5f | ||
|
|
48d0418f0e | ||
|
|
cf0122ce23 | ||
|
|
7216a25877 | ||
|
|
89eae0807e | ||
|
|
bfc52fbc6b | ||
|
|
94cd5d3395 | ||
|
|
30d34584bf |
104
.eslintrc.json
104
.eslintrc.json
@@ -3,60 +3,21 @@
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json", "tsconfig.commonjs.json"],
|
||||
"sourceType": "module"
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"indent": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed"
|
||||
],
|
||||
"brace-style": "off",
|
||||
"@typescript-eslint/brace-style": [
|
||||
"error",
|
||||
"1tbs", { "allowSingleLine": true }
|
||||
],
|
||||
"comma-spacing": "off",
|
||||
"@typescript-eslint/comma-spacing": "error",
|
||||
"space-infix-ops": "error",
|
||||
"comma-dangle": "off",
|
||||
"eqeqeq": [
|
||||
@@ -71,5 +32,58 @@
|
||||
"no-var": "error",
|
||||
"spaced-comment": "error",
|
||||
"semi": "warn"
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json", "tsconfig.commonjs.json"],
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/brace-style": [
|
||||
"error",
|
||||
"1tbs", { "allowSingleLine": true }
|
||||
],
|
||||
"@typescript-eslint/comma-spacing": "error"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -99,6 +99,11 @@ and navigate to `build/viewer`
|
||||
|
||||
node lib/commonjs/cli/lipid-params -o src/mol-model/structure/model/types/lipids.ts
|
||||
|
||||
**Ion names**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-ions.js src/mol-model/structure/model/types/ions.ts
|
||||
|
||||
|
||||
**GraphQL schemas**
|
||||
|
||||
node node_modules//@graphql-codegen/cli/bin -c src/extensions/rcsb/graphql/codegen.yml
|
||||
@@ -106,7 +111,7 @@ and navigate to `build/viewer`
|
||||
### Other scripts
|
||||
**Create chem comp bond table**
|
||||
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-bond/create-table.js build/data/ccb.bcif -b
|
||||
node --max-old-space-size=4096 lib/commonjs/cli/chem-comp-dict/create-table.js build/data/ccb.bcif -b
|
||||
|
||||
**Test model server**
|
||||
|
||||
@@ -172,3 +177,4 @@ Funding sources include but are not limited to:
|
||||
* [RCSB PDB](https://www.rcsb.org) funding by a grant [DBI-1338415; PI: SK Burley] from the NSF, the NIH, and the US DoE
|
||||
* [PDBe, EMBL-EBI](https://pdbe.org)
|
||||
* [CEITEC](https://www.ceitec.eu/)
|
||||
* [EntosAI](https://www.entos.ai) (``alpha-orbitals`` extension)
|
||||
|
||||
@@ -24,7 +24,7 @@ npm run build-tsc
|
||||
and run the server by
|
||||
|
||||
```
|
||||
node lib/servers/model/server/server
|
||||
node lib/commonjs/servers/model/server/server
|
||||
```
|
||||
|
||||
## From NPM
|
||||
|
||||
30349
package-lock.json
generated
30349
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
69
package.json
69
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "1.1.2",
|
||||
"version": "1.2.4",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -11,8 +11,8 @@
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint ./**/*.{ts,tsx}",
|
||||
"lint-fix": "eslint ./**/*.{ts,tsx} --fix",
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"test": "npm run lint && jest",
|
||||
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
|
||||
"build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
|
||||
@@ -29,11 +29,11 @@
|
||||
"watch-webpack": "webpack -w --mode development --display minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --display errors-only --info-verbosity verbose --config ./webpack.config.viewer.debug.js",
|
||||
"serve": "http-server -p 1338",
|
||||
"serve": "http-server -p 1338 -g",
|
||||
"model-server": "node lib/commonjs/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
|
||||
"volume-server-test": "node lib/commonjs/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
|
||||
"plugin-state": "node lib/commonjs/servers/plugin-state/index.js",
|
||||
"plugin-state": "node lib/commonjs/servers/plugin-state/index.js --working-folder ./build/state --port 1339",
|
||||
"preversion": "npm run test",
|
||||
"version": "npm run build",
|
||||
"postversion": "git push && git push --tags"
|
||||
@@ -86,48 +86,48 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^1.14.0",
|
||||
"@graphql-codegen/cli": "^1.14.0",
|
||||
"@graphql-codegen/time": "^1.14.0",
|
||||
"@graphql-codegen/typescript": "^1.14.0",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^1.14.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^1.14.0",
|
||||
"@graphql-codegen/typescript-operations": "^1.14.0",
|
||||
"@types/cors": "^2.8.6",
|
||||
"@typescript-eslint/eslint-plugin": "^3.0.0",
|
||||
"@typescript-eslint/parser": "^3.0.0",
|
||||
"@graphql-codegen/add": "^1.17.7",
|
||||
"@graphql-codegen/cli": "^1.17.8",
|
||||
"@graphql-codegen/time": "^1.17.10",
|
||||
"@graphql-codegen/typescript": "^1.17.9",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^1.17.8",
|
||||
"@graphql-codegen/typescript-graphql-request": "^1.17.7",
|
||||
"@graphql-codegen/typescript-operations": "^1.17.8",
|
||||
"@types/cors": "^2.8.7",
|
||||
"@typescript-eslint/eslint-plugin": "^3.10.1",
|
||||
"@typescript-eslint/parser": "^3.10.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^5.2.0",
|
||||
"concurrently": "^5.3.0",
|
||||
"cpx2": "^2.0.0",
|
||||
"css-loader": "^3.5.3",
|
||||
"eslint": "^7.0.0",
|
||||
"css-loader": "^3.6.0",
|
||||
"eslint": "^7.8.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.0.0",
|
||||
"fs-extra": "^9.0.0",
|
||||
"graphql": "^15.0.0",
|
||||
"file-loader": "^6.1.0",
|
||||
"fs-extra": "^9.0.1",
|
||||
"graphql": "^15.3.0",
|
||||
"http-server": "^0.12.3",
|
||||
"jest": "^26.0.1",
|
||||
"jest": "^26.4.2",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.14.1",
|
||||
"raw-loader": "^4.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"simple-git": "^2.5.0",
|
||||
"simple-git": "^2.20.1",
|
||||
"style-loader": "^1.2.1",
|
||||
"ts-jest": "^26.0.0",
|
||||
"typescript": "^3.9.3",
|
||||
"webpack": "^4.43.0",
|
||||
"webpack-cli": "^3.3.11",
|
||||
"ts-jest": "^26.3.0",
|
||||
"typescript": "^4.0.2",
|
||||
"webpack": "^4.44.1",
|
||||
"webpack-cli": "^3.3.12",
|
||||
"webpack-version-file-plugin": "^0.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^1.0.38",
|
||||
"@types/benchmark": "^1.0.33",
|
||||
"@types/compression": "1.7.0",
|
||||
"@types/express": "^4.17.6",
|
||||
"@types/express": "^4.17.8",
|
||||
"@types/jest": "^25.2.3",
|
||||
"@types/node": "^14.0.5",
|
||||
"@types/node": "^14.10.1",
|
||||
"@types/node-fetch": "^2.5.7",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react": "^16.9.49",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/swagger-ui-dist": "3.0.5",
|
||||
"argparse": "^1.0.10",
|
||||
@@ -135,14 +135,15 @@
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.17.1",
|
||||
"immer": "^6.0.6",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^7.0.9",
|
||||
"immutable": "^3.8.2",
|
||||
"node-fetch": "^2.6.0",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"rxjs": "^6.5.5",
|
||||
"swagger-ui-dist": "^3.25.4",
|
||||
"tslib": "^2.0.0",
|
||||
"rxjs": "^6.6.3",
|
||||
"swagger-ui-dist": "^3.33.0",
|
||||
"tslib": "^2.0.1",
|
||||
"util.promisify": "^1.0.1",
|
||||
"xhr2": "^0.2.0"
|
||||
}
|
||||
|
||||
@@ -4,40 +4,40 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
const git = require('simple-git')
|
||||
const path = require('path')
|
||||
const fs = require("fs")
|
||||
const fse = require("fs-extra")
|
||||
const git = require('simple-git');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git"
|
||||
const buildDir = path.resolve(__dirname, '../build/')
|
||||
const deployDir = path.resolve(buildDir, 'deploy/')
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/')
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
console.log('\n###', command)
|
||||
stdout.pipe(process.stdout)
|
||||
stderr.pipe(process.stderr)
|
||||
console.log('\n###', command);
|
||||
stdout.pipe(process.stdout);
|
||||
stderr.pipe(process.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
function copyViewer() {
|
||||
console.log('\n###', 'copy viewer files')
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/')
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/')
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true })
|
||||
console.log('\n###', 'copy viewer files');
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/');
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
console.log('\n###', 'create localPath')
|
||||
fs.mkdirSync(localPath, { recursive: true })
|
||||
console.log('\n###', 'create localPath');
|
||||
fs.mkdirSync(localPath, { recursive: true });
|
||||
}
|
||||
|
||||
process.chdir(localPath);
|
||||
|
||||
if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
console.log('\n###', 'clone repository')
|
||||
console.log('\n###', 'clone repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.clone(remoteUrl, localPath)
|
||||
@@ -45,9 +45,9 @@ if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
.exec(copyViewer)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer')
|
||||
.push()
|
||||
.push();
|
||||
} else {
|
||||
console.log('\n###', 'update repository')
|
||||
console.log('\n###', 'update repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.fetch(['--all'])
|
||||
@@ -55,5 +55,5 @@ if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
.exec(copyViewer)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer')
|
||||
.push()
|
||||
.push();
|
||||
}
|
||||
@@ -17,22 +17,9 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var viewer = new DockingViewer('app', {
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: false,
|
||||
layoutShowLeftPanel: true,
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowControls: false,
|
||||
viewportShowSettings: false,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowAnimation: false,
|
||||
});
|
||||
var viewer = new DockingViewer('app', [0x33DD22, 0x1133EE], true);
|
||||
|
||||
function getParam(name, regex) {
|
||||
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
|
||||
@@ -11,12 +11,8 @@ import './index.html';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
@@ -24,9 +20,10 @@ import { PluginStateTransform, PluginStateObject as PSO } from '../../mol-plugin
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Task } from '../../mol-task';
|
||||
import { StateObject } from '../../mol-state';
|
||||
import { ViewportComponent, StructurePreset } from './viewport';
|
||||
import { ViewportComponent, StructurePreset, ShowButtons } from './viewport';
|
||||
import { PluginBehaviors } from '../../mol-plugin/behavior';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
@@ -53,13 +50,25 @@ const DefaultViewerOptions = {
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
class Viewer {
|
||||
plugin: PluginContext
|
||||
|
||||
constructor(elementOrId: string | HTMLElement, options: Partial<ViewerOptions> = {}) {
|
||||
const o = { ...DefaultViewerOptions, ...options };
|
||||
constructor(elementOrId: string | HTMLElement, colors = [Color(0x992211), Color(0xDDDDDD)], showButtons = true) {
|
||||
const o = { ...DefaultViewerOptions, ...{
|
||||
layoutIsExpanded: false,
|
||||
layoutShowControls: false,
|
||||
layoutShowRemoteState: false,
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: false,
|
||||
layoutShowLeftPanel: true,
|
||||
|
||||
viewportShowExpand: true,
|
||||
viewportShowControls: false,
|
||||
viewportShowSettings: false,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowAnimation: false,
|
||||
} };
|
||||
|
||||
const spec: PluginSpec = {
|
||||
actions: [...DefaultPluginSpec.actions],
|
||||
@@ -104,7 +113,8 @@ class Viewer {
|
||||
[PluginConfig.State.CurrentServer, o.pluginStateServer],
|
||||
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
|
||||
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider]
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[ShowButtons, showButtons]
|
||||
]
|
||||
};
|
||||
|
||||
@@ -114,40 +124,27 @@ class Viewer {
|
||||
if (!element) throw new Error(`Could not get element with id '${elementOrId}'`);
|
||||
this.plugin = createPlugin(element, spec);
|
||||
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: {
|
||||
renderer: {
|
||||
...this.plugin.canvas3d!.props.renderer,
|
||||
backgroundColor: ColorNames.white,
|
||||
},
|
||||
camera: {
|
||||
...this.plugin.canvas3d!.props.camera,
|
||||
helper: { axes: { name: 'off', params: {} } }
|
||||
(this.plugin.customState as any) = {
|
||||
colorPalette: {
|
||||
name: 'colors',
|
||||
params: { list: { colors } }
|
||||
}
|
||||
} });
|
||||
}
|
||||
};
|
||||
|
||||
setRemoteSnapshot(id: string) {
|
||||
const url = `${this.plugin.config.get(PluginConfig.State.CurrentServer)}/get/${id}`;
|
||||
return PluginCommands.State.Snapshots.Fetch(this.plugin, { url });
|
||||
}
|
||||
|
||||
loadSnapshotFromUrl(url: string, type: PluginState.SnapshotType) {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'url',
|
||||
params: {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
options: params.source.params.options,
|
||||
}
|
||||
this.plugin.behaviors.canvas3d.initialized.subscribe(v => {
|
||||
if (v) {
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: {
|
||||
renderer: {
|
||||
...this.plugin.canvas3d!.props.renderer,
|
||||
backgroundColor: ColorNames.white,
|
||||
},
|
||||
camera: {
|
||||
...this.plugin.canvas3d!.props.camera,
|
||||
helper: { axes: { name: 'off', params: {} } }
|
||||
}
|
||||
} });
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
async loadStructuresFromUrlsAndMerge(sources: { url: string, format: BuiltInTrajectoryFormat, isBinary?: boolean }[]) {
|
||||
@@ -162,69 +159,19 @@ class Viewer {
|
||||
|
||||
structures.push({ ref: structureProperties?.ref || structure.ref });
|
||||
}
|
||||
|
||||
// remove current structuresfrom hierarchy as they will be merged
|
||||
// TODO only works with using loadStructuresFromUrlsAndMerge once
|
||||
// need some more API metho to work with the hierarchy
|
||||
this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');
|
||||
|
||||
const dependsOn = structures.map(({ ref }) => ref);
|
||||
const data = this.plugin.state.data.build().toRoot().apply(MergeStructures, { structures }, { dependsOn });
|
||||
const structure = await data.commit();
|
||||
const structureProperties = await this.plugin.builders.structure.insertStructureProperties(structure);
|
||||
await this.plugin.builders.structure.representation.applyPreset(structureProperties || structure, StructurePreset);
|
||||
}
|
||||
|
||||
async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) {
|
||||
const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format);
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
}
|
||||
|
||||
loadPdb(pdb: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdb,
|
||||
server: {
|
||||
name: provider,
|
||||
params: PdbDownloadProvider[provider].defaultValue as any
|
||||
}
|
||||
},
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadPdbDev(pdbDev: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb-dev' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdbDev,
|
||||
encoding: 'bcif',
|
||||
},
|
||||
options: params.source.params.options,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
loadEmdb(emdb: string) {
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
|
||||
source: {
|
||||
name: 'pdb-emd-ds' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: emdb,
|
||||
server: provider,
|
||||
},
|
||||
detail: 3,
|
||||
}
|
||||
}
|
||||
}));
|
||||
this.plugin.behaviors.canvas3d.initialized.subscribe(async v => {
|
||||
await this.plugin.builders.structure.representation.applyPreset(structureProperties || structure, StructurePreset);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,4 +207,5 @@ const MergeStructures = PluginStateTransform.BuiltIn({
|
||||
}
|
||||
});
|
||||
|
||||
(window as any).DockingViewer = Viewer;
|
||||
(window as any).DockingViewer = Viewer;
|
||||
export { Viewer as DockingViewer };
|
||||
@@ -17,10 +17,11 @@ import { StructureSelectionQueries, StructureSelectionQuery } from '../../mol-pl
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { InteractionsRepresentationProvider } from '../../mol-model-props/computed/representations/interactions';
|
||||
import { InteractionTypeColorThemeProvider } from '../../mol-model-props/computed/themes/interaction-type';
|
||||
import { compile } from '../../mol-script/runtime/query/compiler';
|
||||
import { StructureSelection, QueryContext, Structure } from '../../mol-model/structure';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRef } from '../../mol-plugin-state/manager/structure/hierarchy-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
|
||||
function shinyStyle(plugin: PluginContext) {
|
||||
return PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
|
||||
@@ -76,6 +77,8 @@ const PresetParams = {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
};
|
||||
|
||||
|
||||
|
||||
export const StructurePreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure',
|
||||
display: { name: 'Structure' },
|
||||
@@ -89,10 +92,10 @@ export const StructurePreset = StructureRepresentationPresetProvider({
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams, color } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color }, { tag: 'polymer' }),
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.35 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams }, color: 'chain-id', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
@@ -112,12 +115,14 @@ export const IllustrativePreset = StructureRepresentationPresetProvider({
|
||||
if (!structureCell) return {};
|
||||
|
||||
const components = {
|
||||
all: await presetStaticComponent(plugin, structureCell, 'all')
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
all: builder.buildRepresentation(update, components.all, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative' }, { tag: 'all' }),
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'spacefill', typeParams: { ...typeParams }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'spacefill', typeParams: { ...typeParams }, color: 'illustrative', colorParams: { palette: (plugin.customState as any).colorPalette } }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
@@ -128,6 +133,34 @@ export const IllustrativePreset = StructureRepresentationPresetProvider({
|
||||
}
|
||||
});
|
||||
|
||||
const SurfacePreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-surface',
|
||||
display: { name: 'Surface' },
|
||||
params: () => PresetParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
polymer: await presetStaticComponent(plugin, structureCell, 'polymer'),
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
polymer: builder.buildRepresentation(update, components.polymer, { type: 'molecular-surface', typeParams: { ...typeParams, quality: 'custom', resolution: 0.5, doubleSided: true }, color: 'partial-charge' }, { tag: 'polymer' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'residue' });
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
const PocketPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-pocket',
|
||||
display: { name: 'Pocket' },
|
||||
@@ -144,7 +177,7 @@ const PocketPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }),
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
surroundings: builder.buildRepresentation(update, components.surroundings, { type: 'molecular-surface', typeParams: { ...typeParams, includeParent: true, quality: 'custom', resolution: 0.2, doubleSided: true }, color: 'partial-charge' }, { tag: 'surroundings' }),
|
||||
};
|
||||
|
||||
@@ -152,11 +185,6 @@ const PocketPreset = StructureRepresentationPresetProvider({
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression);
|
||||
const result = compiled(new QueryContext(structure));
|
||||
const selection = StructureSelection.unionStructure(result);
|
||||
plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection));
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
@@ -172,56 +200,46 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
const components = {
|
||||
ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
|
||||
selection: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `selection`)
|
||||
surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
|
||||
interactions: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `interactions`)
|
||||
};
|
||||
|
||||
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
|
||||
const representations = {
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.26 }, color: 'partial-charge' }, { tag: 'ligand' }),
|
||||
ballAndStick: builder.buildRepresentation(update, components.selection, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'partial-charge' }, { tag: 'ball-and-stick' }),
|
||||
interactions: builder.buildRepresentation(update, components.selection, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
|
||||
ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
|
||||
ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
|
||||
interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
|
||||
label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
|
||||
};
|
||||
|
||||
await update.commit({ revertOnError: true });
|
||||
await shinyStyle(plugin);
|
||||
plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
const compiled = compile<StructureSelection>(StructureSelectionQueries.ligand.expression);
|
||||
const result = compiled(new QueryContext(structure));
|
||||
const selection = StructureSelection.unionStructure(result);
|
||||
plugin.managers.camera.focusLoci(Structure.toStructureElementLoci(selection));
|
||||
|
||||
return { components, representations };
|
||||
}
|
||||
});
|
||||
|
||||
export const ShowButtons = PluginConfig.item('showButtons', true);
|
||||
|
||||
export class ViewportComponent extends PluginUIComponent {
|
||||
structurePreset = () => {
|
||||
this.plugin.managers.structure.component.applyPreset(
|
||||
this.plugin.managers.structure.hierarchy.selection.structures,
|
||||
StructurePreset
|
||||
);
|
||||
async _set(structures: readonly StructureRef[], preset: StructureRepresentationPresetProvider) {
|
||||
await this.plugin.managers.structure.component.clear(structures);
|
||||
await this.plugin.managers.structure.component.applyPreset(structures, preset);
|
||||
}
|
||||
|
||||
illustrativePreset = () => {
|
||||
this.plugin.managers.structure.component.applyPreset(
|
||||
this.plugin.managers.structure.hierarchy.selection.structures,
|
||||
IllustrativePreset
|
||||
);
|
||||
set = async (preset: StructureRepresentationPresetProvider) => {
|
||||
await this._set(this.plugin.managers.structure.hierarchy.selection.structures, preset);
|
||||
}
|
||||
|
||||
pocketPreset = () => {
|
||||
this.plugin.managers.structure.component.applyPreset(
|
||||
this.plugin.managers.structure.hierarchy.selection.structures,
|
||||
PocketPreset
|
||||
);
|
||||
}
|
||||
structurePreset = () => this.set(StructurePreset);
|
||||
illustrativePreset = () => this.set(IllustrativePreset);
|
||||
surfacePreset = () => this.set(SurfacePreset);
|
||||
pocketPreset = () => this.set(PocketPreset);
|
||||
interactionsPreset = () => this.set(InteractionsPreset);
|
||||
|
||||
interactionsPreset = () => {
|
||||
this.plugin.managers.structure.component.applyPreset(
|
||||
this.plugin.managers.structure.hierarchy.selection.structures,
|
||||
InteractionsPreset
|
||||
);
|
||||
get showButtons () {
|
||||
return this.plugin.config.get(ShowButtons);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -229,7 +247,7 @@ export class ViewportComponent extends PluginUIComponent {
|
||||
|
||||
return <>
|
||||
<Viewport />
|
||||
<div className='msp-viewport-top-left-controls'>
|
||||
{this.showButtons && <div className='msp-viewport-top-left-controls'>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.structurePreset} >Structure</Button>
|
||||
</div>
|
||||
@@ -237,12 +255,15 @@ export class ViewportComponent extends PluginUIComponent {
|
||||
<Button onClick={this.illustrativePreset}>Illustrative</Button>
|
||||
</div>
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.pocketPreset}>Pocket</Button>
|
||||
<Button onClick={this.surfacePreset}>Surface</Button>
|
||||
</div>
|
||||
{/* <div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.pocketPreset}>Pocket</Button>
|
||||
</div> */}
|
||||
<div style={{ marginBottom: '4px' }}>
|
||||
<Button onClick={this.interactionsPreset}>Interactions</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
<VPControls />
|
||||
<BackgroundTaskProgress />
|
||||
<div className='msp-highlight-toast-wrapper'>
|
||||
@@ -36,28 +36,8 @@
|
||||
emdbProvider: 'rcsb',
|
||||
});
|
||||
viewer.loadPdb('7bv2');
|
||||
viewer.loadEmdb('EMD-30210');
|
||||
|
||||
// TODO add Volume.customProperty and load suggested isoValue via custom property
|
||||
var sub = viewer.plugin.managers.volume.hierarchy.behaviors.selection.subscribe(function (value) {
|
||||
if (value.volume?.representations[0]) {
|
||||
var ref = value.volume.representations[0].cell;
|
||||
var tree = viewer.plugin.state.data.build().to(ref).update({
|
||||
type: {
|
||||
name: 'isosurface',
|
||||
params: {
|
||||
isoValue: {
|
||||
kind: 'relative',
|
||||
relativeValue: 6
|
||||
}
|
||||
}
|
||||
},
|
||||
colorTheme: ref.transform.params?.colorTheme
|
||||
});
|
||||
viewer.plugin.runTask(viewer.plugin.state.data.updateTree(tree));
|
||||
if (typeof sub !== 'undefined') sub.unsubscribe();
|
||||
}
|
||||
});
|
||||
viewer.loadEmdb('EMD-30210', { detail: 6 });
|
||||
// viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,12 +46,18 @@
|
||||
}
|
||||
|
||||
var debugMode = getParam('debug-mode', '[^&]+').trim() === '1';
|
||||
if (debugMode) molstar.setDebugMode(debugMode);
|
||||
if (debugMode) molstar.setDebugMode(debugMode, debugMode);
|
||||
|
||||
var disableAntialiasing = getParam('disable-antialiasing', '[^&]+').trim() === '1';
|
||||
var pixelScale = parseFloat(getParam('pixel-scale', '[^&]+').trim() || '1');
|
||||
var enableWboit = getParam('enable-wboit', '[^&]+').trim() === '1';
|
||||
var hideControls = getParam('hide-controls', '[^&]+').trim() === '1';
|
||||
var pdbProvider = getParam('pdb-provider', '[^&]+').trim().toLowerCase();
|
||||
var emdbProvider = getParam('emdb-provider', '[^&]+').trim().toLowerCase();
|
||||
var viewer = new molstar.Viewer('app', {
|
||||
disableAntialiasing: disableAntialiasing,
|
||||
pixelScale: pixelScale,
|
||||
enableWboit: enableWboit,
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
pdbProvider: pdbProvider || 'pdbe',
|
||||
|
||||
@@ -26,22 +26,39 @@ import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setProductionMode, setDebugMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
const Extensions = {
|
||||
'cellpack': PluginSpec.Behavior(CellPack),
|
||||
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
|
||||
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation)
|
||||
'anvil-membrane-orientation': PluginSpec.Behavior(ANVILMembraneOrientation),
|
||||
'g3d': PluginSpec.Behavior(G3DFormat),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export)
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
@@ -50,6 +67,9 @@ const DefaultViewerOptions = {
|
||||
layoutShowSequence: true,
|
||||
layoutShowLog: true,
|
||||
layoutShowLeftPanel: true,
|
||||
disableAntialiasing: false,
|
||||
pixelScale: 1,
|
||||
enableWboit: false,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
@@ -58,6 +78,7 @@ const DefaultViewerOptions = {
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
volumeStreamingServer: PluginConfig.VolumeStreaming.DefaultServer.defaultValue,
|
||||
volumeStreamingDisabled: !PluginConfig.VolumeStreaming.Enabled.defaultValue,
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
};
|
||||
@@ -77,6 +98,7 @@ export class Viewer {
|
||||
],
|
||||
animations: [...DefaultPluginSpec.animations || []],
|
||||
customParamEditors: DefaultPluginSpec.customParamEditors,
|
||||
customFormats: o?.customFormats,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: o.layoutIsExpanded,
|
||||
@@ -95,6 +117,9 @@ export class Viewer {
|
||||
remoteState: o.layoutShowRemoteState ? 'default' : 'none',
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
|
||||
[PluginConfig.General.PixelScale, o.pixelScale],
|
||||
[PluginConfig.General.EnableWboit, o.enableWboit],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
@@ -103,6 +128,7 @@ export class Viewer {
|
||||
[PluginConfig.State.DefaultServer, o.pluginStateServer],
|
||||
[PluginConfig.State.CurrentServer, o.pluginStateServer],
|
||||
[PluginConfig.VolumeStreaming.DefaultServer, o.volumeStreamingServer],
|
||||
[PluginConfig.VolumeStreaming.Enabled, !o.volumeStreamingDisabled],
|
||||
[PluginConfig.Download.DefaultPdbProvider, o.pdbProvider],
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider]
|
||||
]
|
||||
@@ -124,7 +150,7 @@ export class Viewer {
|
||||
return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
|
||||
}
|
||||
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false) {
|
||||
loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
@@ -133,19 +159,28 @@ export class Viewer {
|
||||
url: Asset.Url(url),
|
||||
format: format as any,
|
||||
isBinary,
|
||||
options: params.source.params.options,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async loadAllModelsOrAssemblyFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
const data = await plugin.builders.data.download({ url, isBinary }, { state: { isGhost: true } });
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, format);
|
||||
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'all-models', { useDefaultIfSingleModel: true, representationPresetParams: options?.representationParams });
|
||||
}
|
||||
|
||||
async loadStructureFromData(data: string | number[], format: BuiltInTrajectoryFormat, options?: { dataLabel?: string }) {
|
||||
const _data = await this.plugin.builders.data.rawData({ data, label: options?.dataLabel });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(_data, format);
|
||||
await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
}
|
||||
|
||||
loadPdb(pdb: string) {
|
||||
loadPdb(pdb: string, options?: LoadStructureOptions) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultPdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
@@ -159,7 +194,7 @@ export class Viewer {
|
||||
params: PdbDownloadProvider[provider].defaultValue as any
|
||||
}
|
||||
},
|
||||
options: params.source.params.options,
|
||||
options: { ...params.source.params.options, representationParams: options?.representationParams as any },
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -181,7 +216,7 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
loadEmdb(emdb: string) {
|
||||
loadEmdb(emdb: string, options?: { detail?: number }) {
|
||||
const provider = this.plugin.config.get(PluginConfig.Download.DefaultEmdbProvider)!;
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadDensity, {
|
||||
source: {
|
||||
@@ -191,9 +226,52 @@ export class Viewer {
|
||||
id: emdb,
|
||||
server: provider,
|
||||
},
|
||||
detail: 3,
|
||||
detail: options?.detail ?? 3,
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async loadVolumeFromUrl(url: string, format: BuildInVolumeFormat, isBinary: boolean, isovalues: VolumeIsovalueInfo[], entryId?: string) {
|
||||
const plugin = this.plugin;
|
||||
|
||||
if (!plugin.dataFormats.get(format)) {
|
||||
throw new Error(`Unknown density format: ${format}`);
|
||||
}
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.builders.data.download({ url, isBinary, label: entryId }, { state: { isGhost: true } });
|
||||
|
||||
const parsed = await plugin.dataFormats.get(format)!.parse(plugin, data, { entryId });
|
||||
const volume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!volume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const repr = plugin.build().to(volume);
|
||||
for (const iso of isovalues) {
|
||||
repr.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, volume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: iso.alpha ?? 1, isoValue: iso.type === 'absolute' ? { kind: 'absolute', absoluteValue: iso.value } : { kind: 'relative', relativeValue: iso.value } },
|
||||
color: 'uniform',
|
||||
colorParams: { value: iso.color }
|
||||
}));
|
||||
}
|
||||
|
||||
await repr.commit();
|
||||
});
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next();
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadStructureOptions {
|
||||
representationParams?: StructureRepresentationPresetProvider.CommonParams
|
||||
}
|
||||
|
||||
export interface VolumeIsovalueInfo {
|
||||
type: 'absolute' | 'relative',
|
||||
value: number,
|
||||
color: Color,
|
||||
alpha?: number
|
||||
}
|
||||
73
src/cli/chem-comp-dict/create-ions.ts
Normal file
73
src/cli/chem-comp-dict/create-ions.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
import { ensureDataAvailable, readCCD } from './util';
|
||||
|
||||
function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
const ionNames: string[] = [];
|
||||
for (const k in ccd) {
|
||||
const {chem_comp} = ccd[k];
|
||||
if (chem_comp.name.value(0).toUpperCase().includes(' ION')) {
|
||||
ionNames.push(chem_comp.id.value(0));
|
||||
}
|
||||
}
|
||||
// these are extra ions that don't have ION in their name
|
||||
ionNames.push('NCO', 'OHX');
|
||||
return ionNames;
|
||||
}
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
* @author molstar/chem-comp-dict/create-table cli
|
||||
*/
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, forceDownload = false) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
const ccd = await readCCD();
|
||||
const ionNames = extractIonNames(ccd);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeIonNamesFile(out, ionNames);
|
||||
}
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
addHelp: true,
|
||||
description: 'Extract and save IonNames from CCD.'
|
||||
});
|
||||
parser.addArgument('out', {
|
||||
help: 'Generated file output path.'
|
||||
});
|
||||
parser.addArgument([ '--forceDownload', '-f' ], {
|
||||
action: 'storeTrue',
|
||||
help: 'Force download of CCD and PVCD.'
|
||||
});
|
||||
interface Args {
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
}
|
||||
const args: Args = parser.parseArgs();
|
||||
|
||||
run(args.out, args.forceDownload);
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,69 +9,16 @@ import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { CifWriter } from '../../mol-io/writer/cif';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
import { SetUtils } from '../../mol-util/set';
|
||||
import { DefaultMap } from '../../mol-util/map';
|
||||
import { mmCIF_chemCompBond_schema } from '../../mol-io/reader/cif/schema/mmcif-extras';
|
||||
|
||||
export async function ensureAvailable(path: string, url: string) {
|
||||
if (FORCE_DOWNLOAD || !fs.existsSync(path)) {
|
||||
console.log(`downloading ${url}...`);
|
||||
const data = await fetch(url);
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDataAvailable() {
|
||||
await ensureAvailable(CCD_PATH, CCD_URL);
|
||||
await ensureAvailable(PVCD_PATH, PVCD_URL);
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
export async function readCCD() {
|
||||
return readFileAsCollection(CCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
export async function readPVCD() {
|
||||
return readFileAsCollection(PVCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
async function parseCif(data: string | Uint8Array) {
|
||||
const comp = CIF.parse(data);
|
||||
console.time('parse cif');
|
||||
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
|
||||
console.timeEnd('parse cif');
|
||||
if (parsed.isError) throw parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getEncodedCif(name: string, database: Database<Database.Schema>, binary = false) {
|
||||
const encoder = CifWriter.createEncoder({ binary, encoderName: 'mol*' });
|
||||
CifWriter.Encoder.writeDatabase(encoder, name, database);
|
||||
return encoder.getData();
|
||||
}
|
||||
import { ccd_chemCompAtom_schema } from '../../mol-io/reader/cif/schema/ccd-extras';
|
||||
import { ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
|
||||
|
||||
type CCB = Table<CCD_Schema['chem_comp_bond']>
|
||||
type CCA = Table<CCD_Schema['chem_comp_atom']>
|
||||
@@ -80,6 +27,10 @@ function ccbKey(compId: string, atomId1: string, atomId2: string) {
|
||||
return atomId1 < atomId2 ? `${compId}:${atomId1}-${atomId2}` : `${compId}:${atomId2}-${atomId1}`;
|
||||
}
|
||||
|
||||
function ccaKey(compId: string, atomId: string) {
|
||||
return `${compId}:${atomId}`;
|
||||
}
|
||||
|
||||
function addChemCompBondToSet(set: Set<string>, ccb: CCB) {
|
||||
for (let i = 0, il = ccb._rowCount; i < il; ++i) {
|
||||
set.add(ccbKey(ccb.comp_id.value(i), ccb.atom_id_1.value(i), ccb.atom_id_2.value(i)));
|
||||
@@ -89,7 +40,7 @@ function addChemCompBondToSet(set: Set<string>, ccb: CCB) {
|
||||
|
||||
function addChemCompAtomToSet(set: Set<string>, cca: CCA) {
|
||||
for (let i = 0, il = cca._rowCount; i < il; ++i) {
|
||||
set.add(cca.atom_id.value(i));
|
||||
set.add(ccaKey(cca.comp_id.value(i), cca.atom_id.value(i)));
|
||||
}
|
||||
return set;
|
||||
}
|
||||
@@ -135,11 +86,32 @@ function checkAddingBondsFromPVCD(pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function createBonds() {
|
||||
await ensureDataAvailable();
|
||||
const ccd = await readCCD();
|
||||
const pvcd = await readPVCD();
|
||||
function checkAddingAtomsFromPVCD(pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
const ccaSetByParent = DefaultMap<string, Set<string>>(() => new Set());
|
||||
|
||||
for (const k in pvcd) {
|
||||
const { chem_comp, chem_comp_atom } = pvcd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
const parentIds = chem_comp.mon_nstd_parent_comp_id.value(0);
|
||||
if (parentIds.length === 0) {
|
||||
const set = ccaSetByParent.getDefault(chem_comp.id.value(0));
|
||||
addChemCompAtomToSet(set, chem_comp_atom);
|
||||
} else {
|
||||
for (let i = 0, il = parentIds.length; i < il; ++i) {
|
||||
const parentId = parentIds[i];
|
||||
const set = ccaSetByParent.getDefault(parentId);
|
||||
addChemCompAtomToSet(set, chem_comp_atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createBonds(
|
||||
ccd: DatabaseCollection<CCD_Schema>,
|
||||
pvcd: DatabaseCollection<CCD_Schema>,
|
||||
atomsRequested: boolean
|
||||
) {
|
||||
const ccbSet = new Set<string>();
|
||||
|
||||
const comp_id: string[] = [];
|
||||
@@ -200,31 +172,97 @@ async function createBonds() {
|
||||
});
|
||||
|
||||
const bondDatabase = Database.ofTables(
|
||||
TABLE_NAME,
|
||||
CCB_TABLE_NAME,
|
||||
{ chem_comp_bond: mmCIF_chemCompBond_schema },
|
||||
{ chem_comp_bond: bondTable }
|
||||
);
|
||||
|
||||
return bondDatabase;
|
||||
return { bonds: bondDatabase, atoms: atomsRequested ? createAtoms(ccd, pvcd) : void 0 };
|
||||
}
|
||||
|
||||
async function run(out: string, binary = false) {
|
||||
const bonds = await createBonds();
|
||||
function createAtoms(ccd: DatabaseCollection<CCD_Schema>, pvcd: DatabaseCollection<CCD_Schema>) {
|
||||
const ccaSet = new Set<string>();
|
||||
|
||||
const cif = getEncodedCif(TABLE_NAME, bonds, binary);
|
||||
const comp_id: string[] = [];
|
||||
const atom_id: string[] = [];
|
||||
const charge: number[] = [];
|
||||
const pdbx_stereo_config: typeof CCD_Schema.chem_comp_atom['pdbx_stereo_config']['T'][] = [];
|
||||
|
||||
function addAtoms(compId: string, cca: CCA) {
|
||||
for (let i = 0, il = cca._rowCount; i < il; ++i) {
|
||||
const atomId = cca.atom_id.value(i);
|
||||
const k = ccaKey(compId, atomId);
|
||||
if (!ccaSet.has(k)) {
|
||||
atom_id.push(atomId);
|
||||
comp_id.push(compId);
|
||||
charge.push(cca.charge.value(i));
|
||||
pdbx_stereo_config.push(cca.pdbx_stereo_config.value(i));
|
||||
ccaSet.add(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check adding atoms from PVCD
|
||||
checkAddingAtomsFromPVCD(pvcd);
|
||||
|
||||
// add atoms from PVCD
|
||||
for (const k in pvcd) {
|
||||
const { chem_comp, chem_comp_atom } = pvcd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
const parentIds = chem_comp.mon_nstd_parent_comp_id.value(0);
|
||||
if (parentIds.length === 0) {
|
||||
addAtoms(chem_comp.id.value(0), chem_comp_atom);
|
||||
} else {
|
||||
for (let i = 0, il = parentIds.length; i < il; ++i) {
|
||||
addAtoms(parentIds[i], chem_comp_atom);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// add atoms from CCD
|
||||
for (const k in ccd) {
|
||||
const { chem_comp, chem_comp_atom } = ccd[k];
|
||||
if (chem_comp_atom._rowCount) {
|
||||
addAtoms(chem_comp.id.value(0), chem_comp_atom);
|
||||
}
|
||||
}
|
||||
|
||||
const atomTable = Table.ofArrays(ccd_chemCompAtom_schema, {
|
||||
comp_id, atom_id, charge, pdbx_stereo_config
|
||||
});
|
||||
|
||||
return Database.ofTables(
|
||||
CCA_TABLE_NAME,
|
||||
{ chem_comp_atom: ccd_chemCompAtom_schema },
|
||||
{ chem_comp_atom: atomTable }
|
||||
);
|
||||
}
|
||||
|
||||
async function run(out: string, binary = false, forceDownload = false, ccaOut?: string) {
|
||||
await ensureDataAvailable(forceDownload);
|
||||
const ccd = await readCCD();
|
||||
const pvcd = await readPVCD();
|
||||
|
||||
const { bonds, atoms } = await createBonds(ccd, pvcd, !!ccaOut);
|
||||
|
||||
const ccbCif = getEncodedCif(CCB_TABLE_NAME, bonds, binary);
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeFile(out, cif);
|
||||
writeFile(out, ccbCif);
|
||||
|
||||
if (!!ccaOut) {
|
||||
const ccaCif = getEncodedCif(CCA_TABLE_NAME, atoms, binary);
|
||||
if (!fs.existsSync(path.dirname(ccaOut))) {
|
||||
fs.mkdirSync(path.dirname(ccaOut));
|
||||
}
|
||||
writeFile(ccaOut, ccaCif);
|
||||
}
|
||||
}
|
||||
|
||||
const TABLE_NAME = 'CHEM_COMP_BONDS';
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
|
||||
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
|
||||
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
const CCB_TABLE_NAME = 'CHEM_COMP_BONDS';
|
||||
const CCA_TABLE_NAME = 'CHEM_COMP_ATOMS';
|
||||
|
||||
const parser = new argparse.ArgumentParser({
|
||||
addHelp: true,
|
||||
@@ -241,13 +279,16 @@ parser.addArgument([ '--binary', '-b' ], {
|
||||
action: 'storeTrue',
|
||||
help: 'Output as BinaryCIF.'
|
||||
});
|
||||
parser.addArgument(['--ccaOut', '-a'], {
|
||||
help: 'Optional generated file output path for chem_comp_atom data.',
|
||||
required: false
|
||||
});
|
||||
interface Args {
|
||||
out: string
|
||||
forceDownload?: boolean
|
||||
binary?: boolean
|
||||
out: string,
|
||||
forceDownload?: boolean,
|
||||
binary?: boolean,
|
||||
ccaOut?: string
|
||||
}
|
||||
const args: Args = parser.parseArgs();
|
||||
|
||||
const FORCE_DOWNLOAD = args.forceDownload;
|
||||
|
||||
run(args.out, args.binary);
|
||||
run(args.out, args.binary, args.forceDownload, args.ccaOut);
|
||||
75
src/cli/chem-comp-dict/util.ts
Normal file
75
src/cli/chem-comp-dict/util.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database } from '../../mol-data/db';
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { CifWriter } from '../../mol-io/writer/cif';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
|
||||
export async function ensureAvailable(path: string, url: string, forceDownload = false) {
|
||||
if (forceDownload || !fs.existsSync(path)) {
|
||||
console.log(`downloading ${url}...`);
|
||||
const data = await fetch(url);
|
||||
if (!fs.existsSync(DATA_DIR)) {
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureDataAvailable(forceDownload = false) {
|
||||
await ensureAvailable(CCD_PATH, CCD_URL, forceDownload);
|
||||
await ensureAvailable(PVCD_PATH, PVCD_URL, forceDownload);
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
export async function readCCD() {
|
||||
return readFileAsCollection(CCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
export async function readPVCD() {
|
||||
return readFileAsCollection(PVCD_PATH, CCD_Schema);
|
||||
}
|
||||
|
||||
async function parseCif(data: string | Uint8Array) {
|
||||
const comp = CIF.parse(data);
|
||||
console.time('parse cif');
|
||||
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
|
||||
console.timeEnd('parse cif');
|
||||
if (parsed.isError) throw parsed;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
export function getEncodedCif(name: string, database: Database<Database.Schema>, binary = false) {
|
||||
const encoder = CifWriter.createEncoder({ binary, encoderName: 'mol*' });
|
||||
CifWriter.Encoder.writeDatabase(encoder, name, database);
|
||||
return encoder.getData();
|
||||
}
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
|
||||
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
|
||||
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
@@ -31,6 +31,8 @@ async function ensureAvailable(path: string, url: string) {
|
||||
|
||||
async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PATH, MARTINI_LIPIDS_URL); }
|
||||
|
||||
const extraLipids = ['DMPC'];
|
||||
|
||||
async function run(out: string) {
|
||||
await ensureLipidsAvailable();
|
||||
const lipidsItpStr = fs.readFileSync(MARTINI_LIPIDS_PATH, 'utf8');
|
||||
@@ -44,17 +46,20 @@ async function run(out: string) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
for (const v of extraLipids) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
const lipidNames = JSON.stringify(lipids.array);
|
||||
|
||||
if (out) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
|
||||
*
|
||||
* @author molstar/lipid-params cli
|
||||
*/
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
|
||||
*
|
||||
* @author molstar/lipid-params cli
|
||||
*/
|
||||
|
||||
export const LipidNames = new Set(${lipidNames.replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
|
||||
@@ -26,6 +26,8 @@ function paramInfo(param: PD.Any, offset: number): string {
|
||||
case 'file': return `JavaScript File Handle`;
|
||||
case 'file-list': return `JavaScript FileList Handle`;
|
||||
case 'select': return `One of ${oToS(param.options)}`;
|
||||
case 'value-ref': return `Reference to a runtime defined value.`;
|
||||
case 'data-ref': return `Reference to a computed data value.`;
|
||||
case 'text': return 'String';
|
||||
case 'interval': return `Interval [min, max]`;
|
||||
case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`;
|
||||
|
||||
@@ -10,7 +10,7 @@ import * as argparse from 'argparse';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing } from '../../mol-model/structure';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
|
||||
// import { Run, Progress } from '../../mol-task'
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { openCif, downloadCif } from './helpers';
|
||||
@@ -19,6 +19,7 @@ import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
|
||||
import { Sequence } from '../../mol-model/sequence';
|
||||
import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
|
||||
import { ModelSymmetry } from '../../mol-model-formats/structure/property/symmetry';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
|
||||
async function downloadFromPdb(pdb: string) {
|
||||
@@ -98,15 +99,17 @@ export function printBonds(structure: Structure, showIntra: boolean, showInter:
|
||||
for (const unit of structure.units) {
|
||||
if (!Unit.isAtomic(unit)) continue;
|
||||
|
||||
for (const pairBonds of bonds.getConnectedUnits(unit)) {
|
||||
for (const pairBonds of bonds.getConnectedUnits(unit.id)) {
|
||||
if (!pairBonds.areUnitsOrdered || pairBonds.edgeCount === 0) continue;
|
||||
|
||||
const { unitA, unitB } = pairBonds;
|
||||
console.log(`${pairBonds.unitA.id} - ${pairBonds.unitB.id}: ${pairBonds.edgeCount} bond(s)`);
|
||||
const { unitA, unitB, edgeCount } = pairBonds;
|
||||
const uA = structure.unitMap.get(unitA);
|
||||
const uB = structure.unitMap.get(unitB);
|
||||
console.log(`${unitA} - ${unitB}: ${edgeCount} bond(s)`);
|
||||
|
||||
for (const aI of pairBonds.connectedIndices) {
|
||||
for (const bond of pairBonds.getEdges(aI)) {
|
||||
console.log(`${atomLabel(unitA.model, unitA.elements[aI])} -- ${atomLabel(unitB.model, unitB.elements[bond.indexB])}`);
|
||||
console.log(`${atomLabel(uA.model, uA.elements[aI])} -- ${atomLabel(uB.model, uB.elements[bond.indexB])}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,10 +186,11 @@ export function printSymmetryInfo(model: Model) {
|
||||
console.log(`NCS operators: ${symmetry.ncsOperators && symmetry.ncsOperators.map(a => a.name).join(', ')}`);
|
||||
}
|
||||
|
||||
export function printModelStats(models: ReadonlyArray<Model>) {
|
||||
export async function printModelStats(models: Trajectory) {
|
||||
console.log('\nModels\n=============');
|
||||
|
||||
for (const m of models) {
|
||||
for (let i = 0; i < models.frameCount; i++) {
|
||||
const m = await Task.resolveInContext(models.getFrameAtIndex(i));
|
||||
if (m.coarseHierarchy.isDefined) {
|
||||
console.log(`${m.label} ${m.modelNum}: ${m.atomicHierarchy.atoms._rowCount} atom(s), ${m.coarseHierarchy.spheres.count} sphere(s), ${m.coarseHierarchy.gaussians.count} gaussian(s)`);
|
||||
} else {
|
||||
@@ -198,7 +202,7 @@ export function printModelStats(models: ReadonlyArray<Model>) {
|
||||
|
||||
export async function getModelsAndStructure(frame: CifFrame) {
|
||||
const models = await trajectoryFromMmCIF(frame).run();
|
||||
const structure = Structure.ofModel(models[0]);
|
||||
const structure = Structure.ofModel(models.representative);
|
||||
return { models, structure };
|
||||
}
|
||||
|
||||
@@ -206,13 +210,13 @@ async function run(frame: CifFrame, args: Args) {
|
||||
const { models, structure } = await getModelsAndStructure(frame);
|
||||
|
||||
if (args.models) printModelStats(models);
|
||||
if (args.seq) printSequence(models[0]);
|
||||
if (args.seq) printSequence(models.representative);
|
||||
if (args.units) printUnits(structure);
|
||||
if (args.sym) printSymmetryInfo(models[0]);
|
||||
if (args.sym) printSymmetryInfo(models.representative);
|
||||
if (args.rings) printRings(structure);
|
||||
if (args.intraBonds) printBonds(structure, true, false);
|
||||
if (args.interBonds) printBonds(structure, false, true);
|
||||
if (args.sec) printSecStructure(models[0]);
|
||||
if (args.sec) printSecStructure(models.representative);
|
||||
}
|
||||
|
||||
async function runDL(pdb: string, args: Args) {
|
||||
|
||||
25
src/examples/alpha-orbitals/controls.tsx
Normal file
25
src/examples/alpha-orbitals/controls.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { AlphaOrbitalsExample } from '.';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { PluginContextContainer } from '../../mol-plugin-ui/plugin';
|
||||
|
||||
export function mountControls(orbitals: AlphaOrbitalsExample, parent: Element) {
|
||||
ReactDOM.render(<PluginContextContainer plugin={orbitals.plugin}>
|
||||
<Controls orbitals={orbitals} />
|
||||
</PluginContextContainer>, parent);
|
||||
}
|
||||
|
||||
function Controls({ orbitals }: { orbitals: AlphaOrbitalsExample }) {
|
||||
const params = useBehavior(orbitals.params);
|
||||
const values = useBehavior(orbitals.state);
|
||||
|
||||
return <ParameterControls params={params as any} values={values} onChangeValues={(vs: any) => orbitals.state.next(vs)} />;
|
||||
}
|
||||
60420
src/examples/alpha-orbitals/example-data.ts
Normal file
60420
src/examples/alpha-orbitals/example-data.ts
Normal file
File diff suppressed because it is too large
Load Diff
37
src/examples/alpha-orbitals/index.html
Normal file
37
src/examples/alpha-orbitals/index.html
Normal file
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Alpha Orbitals Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
top: 8px;
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id='controls'></div>
|
||||
<script>
|
||||
AlphaOrbitalsExample.init('app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
212
src/examples/alpha-orbitals/index.ts
Normal file
212
src/examples/alpha-orbitals/index.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { SphericalBasisOrder } from '../../extensions/alpha-orbitals/spherical-functions';
|
||||
import { BasisAndOrbitals, CreateOrbitalDensityVolume, CreateOrbitalRepresentation3D, CreateOrbitalVolume, StaticBasisAndOrbitals } from '../../extensions/alpha-orbitals/transforms';
|
||||
import { createPluginAsync, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector, StateTransformer } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { mountControls } from './controls';
|
||||
import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { debounceTime, skip } from 'rxjs/operators';
|
||||
import './index.html';
|
||||
import { Basis, AlphaOrbital } from '../../extensions/alpha-orbitals/data-model';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
|
||||
interface DemoInput {
|
||||
moleculeSdf: string,
|
||||
basis: Basis,
|
||||
order: SphericalBasisOrder,
|
||||
orbitals: AlphaOrbital[]
|
||||
}
|
||||
|
||||
interface Params {
|
||||
show: { name: 'orbital', params: { index: number } } | { name: 'density', params: {} },
|
||||
isoValue: number,
|
||||
gpuSurface: boolean
|
||||
}
|
||||
|
||||
type Selectors = {
|
||||
type: 'orbital',
|
||||
volume: StateObjectSelector<PluginStateObject.Volume.Data, typeof CreateOrbitalVolume>,
|
||||
positive: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
negative: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
} | {
|
||||
type: 'density',
|
||||
volume: StateObjectSelector<PluginStateObject.Volume.Data, typeof CreateOrbitalDensityVolume>,
|
||||
positive: StateObjectSelector<PluginStateObject.Volume.Representation3D, typeof CreateOrbitalRepresentation3D>
|
||||
}
|
||||
|
||||
export class AlphaOrbitalsExample {
|
||||
plugin: PluginContext;
|
||||
|
||||
async init(target: string | HTMLElement) {
|
||||
this.plugin = await createPluginAsync(typeof target === 'string' ? document.getElementById(target)! : target, {
|
||||
...DefaultPluginSpec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
showControls: false
|
||||
},
|
||||
controls: { left: 'none', right: 'none', top: 'none', bottom: 'none' },
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowExpand, false],
|
||||
[PluginConfig.Viewport.ShowControls, false],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, false],
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
});
|
||||
|
||||
this.plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
|
||||
if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) {
|
||||
PluginCommands.Toast.Show(this.plugin, {
|
||||
title: 'Error',
|
||||
message: `Browser/device does not support required WebGL extension (OES_texture_float).`
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.load({
|
||||
moleculeSdf: DemoMoleculeSDF,
|
||||
...DemoOrbitals
|
||||
});
|
||||
|
||||
mountControls(this, document.getElementById('controls')!);
|
||||
}
|
||||
|
||||
readonly params = new BehaviorSubject<ParamDefinition.For<Params>>({} as any);
|
||||
readonly state = new BehaviorSubject<Params>({ show: { name: 'orbital', params: { index: 32 } }, isoValue: 1, gpuSurface: false });
|
||||
|
||||
private selectors?: Selectors = void 0;
|
||||
private basis?: StateObjectSelector<BasisAndOrbitals> = void 0;
|
||||
|
||||
private currentParams: Params = { ...this.state.value };
|
||||
|
||||
private clearVolume() {
|
||||
if (!this.selectors) return;
|
||||
const v = this.selectors.volume;
|
||||
this.selectors = void 0;
|
||||
return this.plugin.build().delete(v).commit();
|
||||
}
|
||||
|
||||
private async syncVolume() {
|
||||
if (!this.basis?.isOk) return;
|
||||
|
||||
const state = this.state.value;
|
||||
|
||||
if (state.show.name !== this.selectors?.type) {
|
||||
await this.clearVolume();
|
||||
}
|
||||
|
||||
const update = this.plugin.build();
|
||||
if (state.show.name === 'orbital') {
|
||||
if (!this.selectors) {
|
||||
const volume = update
|
||||
.to(this.basis)
|
||||
.apply(CreateOrbitalVolume, { index: state.show.params.index });
|
||||
|
||||
const positive = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('positive', ColorNames.blue)).selector;
|
||||
const negative = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('negative', ColorNames.red)).selector;
|
||||
|
||||
this.selectors = { type: 'orbital', volume: volume.selector, positive, negative };
|
||||
} else {
|
||||
const index = state.show.params.index;
|
||||
update.to(this.selectors.volume).update(CreateOrbitalVolume, () => ({ index }));
|
||||
}
|
||||
} else {
|
||||
if (!this.selectors) {
|
||||
const volume = update
|
||||
.to(this.basis)
|
||||
.apply(CreateOrbitalDensityVolume);
|
||||
const positive = volume.apply(CreateOrbitalRepresentation3D, this.volumeParams('positive', ColorNames.blue)).selector;
|
||||
this.selectors = { type: 'density', volume: volume.selector, positive };
|
||||
}
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
|
||||
if (this.currentParams.gpuSurface !== this.state.value.gpuSurface) {
|
||||
await this.setIsovalue();
|
||||
}
|
||||
|
||||
this.currentParams = this.state.value;
|
||||
}
|
||||
|
||||
private setIsovalue() {
|
||||
if (!this.selectors) return;
|
||||
|
||||
this.currentParams = this.state.value;
|
||||
const update = this.plugin.build();
|
||||
update.to(this.selectors.positive).update(this.volumeParams('positive', ColorNames.blue));
|
||||
if (this.selectors?.type === 'orbital') {
|
||||
update.to(this.selectors.negative).update(this.volumeParams('negative', ColorNames.red));
|
||||
}
|
||||
return update.commit();
|
||||
}
|
||||
|
||||
private volumeParams(kind: 'positive' | 'negative', color: Color): StateTransformer.Params<typeof CreateOrbitalRepresentation3D> {
|
||||
return {
|
||||
alpha: 0.85,
|
||||
color,
|
||||
directVolume: this.state.value.gpuSurface,
|
||||
kind,
|
||||
relativeIsovalue: this.state.value.isoValue,
|
||||
pickable: false,
|
||||
xrayShaded: true
|
||||
};
|
||||
}
|
||||
|
||||
async load(input: DemoInput) {
|
||||
await this.plugin.clear();
|
||||
|
||||
const data = await this.plugin.builders.data.rawData({ data: input.moleculeSdf }, { state: { isGhost: true } });
|
||||
const trajectory = await this.plugin.builders.structure.parseTrajectory(data, 'mol');
|
||||
const model = await this.plugin.builders.structure.createModel(trajectory);
|
||||
const structure = await this.plugin.builders.structure.createStructure(model);
|
||||
|
||||
const all = await this.plugin.builders.structure.tryCreateComponentStatic(structure, 'all');
|
||||
if (all) await this.plugin.builders.structure.representation.addRepresentation(all, { type: 'ball-and-stick', color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } });
|
||||
|
||||
|
||||
this.basis = await this.plugin.build().toRoot()
|
||||
.apply(StaticBasisAndOrbitals, { basis: input.basis, order: input.order, orbitals: input.orbitals })
|
||||
.commit();
|
||||
|
||||
await this.syncVolume();
|
||||
|
||||
this.params.next({
|
||||
show: ParamDefinition.MappedStatic('orbital', {
|
||||
'orbital': ParamDefinition.Group({
|
||||
index: ParamDefinition.Numeric(32, { min: 0, max: input.orbitals.length - 1 }, { immediateUpdate: true, isEssential: true }),
|
||||
}),
|
||||
'density': ParamDefinition.EmptyGroup()
|
||||
}, { cycle: true }),
|
||||
isoValue: ParamDefinition.Numeric(this.currentParams.isoValue, { min: 0.5, max: 3, step: 0.1 }, { immediateUpdate: true, isEssential: false }),
|
||||
gpuSurface: ParamDefinition.Boolean(this.currentParams.gpuSurface, { isHidden: true })
|
||||
});
|
||||
|
||||
this.state.pipe(skip(1), debounceTime(1000 / 24)).subscribe(async params => {
|
||||
if (params.show.name !== this.currentParams.show.name
|
||||
|| (params.show.name === 'orbital' && this.currentParams.show.name === 'orbital' && params.show.params.index !== this.currentParams.show.params.index)) {
|
||||
this.syncVolume();
|
||||
} else if (params.isoValue !== this.currentParams.isoValue || params.gpuSurface !== this.currentParams.gpuSurface) {
|
||||
this.setIsovalue();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
|
||||
@@ -7,9 +7,8 @@
|
||||
import { EmptyLoci } from '../../mol-model/loci';
|
||||
import { StructureSelection } from '../../mol-model/structure';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Script } from '../../mol-script/script';
|
||||
@@ -113,8 +112,10 @@ class BasicWrapper {
|
||||
|
||||
interactivity = {
|
||||
highlightOn: () => {
|
||||
const data = this.plugin.managers.structure.hierarchy.current.structures[0]?.cell.obj?.data;
|
||||
if (!data) return;
|
||||
|
||||
const seq_id = 7;
|
||||
const data = (this.plugin.state.data.select('asm')[0].obj as PluginStateObject.Molecule.Structure).data;
|
||||
const sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
'residue-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), seq_id]),
|
||||
'group-by': Q.struct.atomProperty.macromolecular.residueKey()
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { createPlugin, DefaultPluginSpec } from '../../mol-plugin';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in';
|
||||
import { AnimateModelIndex } from '../../mol-plugin-state/animation/built-in/model-index';
|
||||
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
|
||||
import { PluginStateObject, PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
@@ -403,7 +403,7 @@ class MolStarProteopediaWrapper {
|
||||
},
|
||||
download: async (type: 'molj' | 'molx' = 'molj', params?: PluginState.SnapshotParams) => {
|
||||
const data = await this.plugin.managers.snapshot.serialize({ type, params });
|
||||
download(data, `mol-star_state_${(name || getFormattedTime())}.${type}`);
|
||||
download(data, `mol-star_state_${getFormattedTime()}.${type}`);
|
||||
},
|
||||
fetch: async (url: string, type: 'molj' | 'molx' = 'molj') => {
|
||||
try {
|
||||
|
||||
214
src/extensions/alpha-orbitals/_spec/collocation.spec.ts
Normal file
214
src/extensions/alpha-orbitals/_spec/collocation.spec.ts
Normal file
@@ -0,0 +1,214 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Box3D } from '../../../mol-math/geometry';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { sphericalCollocation } from '../collocation';
|
||||
import { Basis, CubeGridInfo } from '../data-model';
|
||||
|
||||
describe('alpha-orbitals-cubes', () => {
|
||||
it('water', async () => {
|
||||
const grid: CubeGridInfo = {
|
||||
params: {
|
||||
basis: _testBasis,
|
||||
cutoffThreshold: 0,
|
||||
sphericalOrder: 'cca-reverse',
|
||||
boxExpand: 0,
|
||||
gridSpacing: []
|
||||
},
|
||||
box: Box3D.create(Vec3.create(-1, -1, -1), Vec3.create(1, 1, 1)),
|
||||
delta: Vec3.create(2, 2, 2),
|
||||
dimensions: Vec3.create(2, 2, 2),
|
||||
npoints: 8,
|
||||
size: Vec3.create(2, 2, 2)
|
||||
};
|
||||
|
||||
const matrix = await sphericalCollocation(grid, {
|
||||
energy: 0,
|
||||
occupancy: 0,
|
||||
alpha: [-2.2623991420609075e-16, 0.6360205395000592, 0.6672122399886391, -0.3876927909355508, -1.6780131293332933e-16, 2.844782862661151e-16, 4.977960694176068e-19, -2.3945919908996803e-16]
|
||||
}, RuntimeContext.Synchronous);
|
||||
|
||||
const expected = [-0.1451730622877498, 0.06479453956039086, -0.2777738736440713, -0.057116584776260436, 0.05929916178822645, 0.2742903371231049, -0.07221698722165386, 0.15389180241391376];
|
||||
|
||||
expect(matrix.length).toBe(expected.length);
|
||||
|
||||
for (let i = 0; i < matrix.length; i++) {
|
||||
expect(Math.abs(matrix[i] - expected[i]) < 1e-6).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const _testBasis: Basis = {
|
||||
'atoms': [
|
||||
{
|
||||
'center': [
|
||||
0.025886090588624934,
|
||||
0.019164790004065606,
|
||||
-0.013539970104105408
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.004151277818987536,
|
||||
-0.02067024147993795,
|
||||
-0.05150303336984537,
|
||||
0.33462711739899537,
|
||||
0.5621061300983125,
|
||||
0.17129946969948573
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
152.28769660788095,
|
||||
27.928015215973073,
|
||||
7.848374792384515,
|
||||
1.1223350202705642,
|
||||
0.5093846587907856,
|
||||
0.24292266532510307
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [1],
|
||||
'coefficients': [
|
||||
[
|
||||
0.007924233646294425,
|
||||
0.051441048251911314,
|
||||
0.18984000600705359,
|
||||
0.4049863191150474,
|
||||
0.40123628611490797,
|
||||
0.1051855189039082
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
27.203421487167727,
|
||||
7.09409912597673,
|
||||
2.5383362605345954,
|
||||
1.0610730767843852,
|
||||
0.4851948916410433,
|
||||
0.22938302550642545
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'center': [
|
||||
0.5082729578468134,
|
||||
1.6880351220025265,
|
||||
0.4963443067810461
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
0.009163596280542963,
|
||||
0.04936149294292479,
|
||||
0.16853830490998634,
|
||||
0.37056279972195677,
|
||||
0.4164915298246781,
|
||||
0.13033408410772263
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.32279868167000036,
|
||||
3.209629817295221,
|
||||
2.4672629224617935,
|
||||
-0.048487066612842224,
|
||||
-0.2611850111200143,
|
||||
-0.8917817597810863,
|
||||
-1.9607480081275706,
|
||||
-2.203769342520311,
|
||||
-0.6896328935259993
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
10.256286070314905,
|
||||
0.6227965325875392,
|
||||
0.2391007667853915,
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'center': [
|
||||
1.1367367844436005,
|
||||
-0.47018519422670163,
|
||||
-1.356802622574504
|
||||
] as Vec3,
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
0.009163596280542963,
|
||||
0.04936149294292479,
|
||||
0.16853830490998634,
|
||||
0.37056279972195677,
|
||||
0.4164915298246781,
|
||||
0.13033408410772263
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
},
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
'coefficients': [
|
||||
[
|
||||
-0.32279868167000036,
|
||||
3.209629817295221,
|
||||
2.4672629224617935,
|
||||
-0.048487066612842224,
|
||||
-0.2611850111200143,
|
||||
-0.8917817597810863,
|
||||
-1.9607480081275706,
|
||||
-2.203769342520311,
|
||||
-0.6896328935259993
|
||||
]
|
||||
],
|
||||
'exponents': [
|
||||
10.256286070314905,
|
||||
0.6227965325875392,
|
||||
0.2391007667853915,
|
||||
33.710073211949485,
|
||||
6.180705022740464,
|
||||
1.7291385346152253,
|
||||
0.5940057549921978,
|
||||
0.2306698170449518,
|
||||
0.09500256906284119
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
162
src/extensions/alpha-orbitals/collocation.ts
Normal file
162
src/extensions/alpha-orbitals/collocation.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { arrayMin } from '../../mol-util/array';
|
||||
import { AlphaOrbital, CubeGridInfo } from './data-model';
|
||||
import { normalizeBasicOrder, SphericalFunctions } from './spherical-functions';
|
||||
|
||||
export async function sphericalCollocation(
|
||||
grid: CubeGridInfo,
|
||||
orbital: AlphaOrbital,
|
||||
taskCtx: RuntimeContext
|
||||
) {
|
||||
const { basis, sphericalOrder, cutoffThreshold } = grid.params;
|
||||
let baseCount = 0;
|
||||
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
if (L > 4) {
|
||||
// TODO: will L > 4 be required? Would need to precompute more functions in that case.
|
||||
throw new Error('Angular momentum L > 4 not supported.');
|
||||
}
|
||||
|
||||
baseCount += 2 * L + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const matrix = new Float32Array(grid.npoints);
|
||||
|
||||
let baseIndex = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
let amIndex = 0;
|
||||
for (const L of shell.angularMomentum) {
|
||||
const alpha = normalizeBasicOrder(
|
||||
L,
|
||||
orbital.alpha.slice(baseIndex, baseIndex + 2 * L + 1),
|
||||
sphericalOrder
|
||||
);
|
||||
baseIndex += 2 * L + 1;
|
||||
|
||||
collocationBasis(
|
||||
matrix,
|
||||
grid,
|
||||
L,
|
||||
shell.coefficients[amIndex++],
|
||||
shell.exponents,
|
||||
atom.center,
|
||||
cutoffThreshold,
|
||||
alpha
|
||||
);
|
||||
|
||||
if (taskCtx.shouldUpdate) {
|
||||
await taskCtx.update({
|
||||
message: 'Computing...',
|
||||
current: baseIndex,
|
||||
max: baseCount,
|
||||
isIndeterminate: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return matrix;
|
||||
}
|
||||
|
||||
function collocationBasis(
|
||||
matrix: Float32Array,
|
||||
grid: CubeGridInfo,
|
||||
L: number,
|
||||
coefficients: number[],
|
||||
exponents: number[],
|
||||
center: Vec3,
|
||||
cutoffThreshold: number,
|
||||
alpha: number[]
|
||||
) {
|
||||
const ncoeff = exponents.length;
|
||||
const sphericalFunc = SphericalFunctions[L];
|
||||
|
||||
const cx = center[0],
|
||||
cy = center[1],
|
||||
cz = center[2];
|
||||
const ny = grid.dimensions[1],
|
||||
nz = grid.dimensions[2];
|
||||
const gdx = grid.delta[0],
|
||||
gdy = grid.delta[1],
|
||||
gdz = grid.delta[2];
|
||||
const sx = grid.box.min[0],
|
||||
sy = grid.box.min[1],
|
||||
sz = grid.box.min[2];
|
||||
|
||||
const cutoffRadius =
|
||||
cutoffThreshold > 0
|
||||
? Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(exponents))
|
||||
: 10000;
|
||||
const cutoffSquared = cutoffRadius * cutoffRadius;
|
||||
|
||||
const radiusBox = getRadiusBox(grid, center, cutoffRadius);
|
||||
const iMin = radiusBox[0][0],
|
||||
jMin = radiusBox[0][1],
|
||||
kMin = radiusBox[0][2];
|
||||
const iMax = radiusBox[1][0],
|
||||
jMax = radiusBox[1][1],
|
||||
kMax = radiusBox[1][2];
|
||||
|
||||
for (let i = iMin; i <= iMax; i++) {
|
||||
const x = sx + gdx * i - cx;
|
||||
const oX = i * ny * nz;
|
||||
|
||||
for (let j = jMin; j <= jMax; j++) {
|
||||
const y = sy + gdy * j - cy;
|
||||
const oY = oX + j * nz;
|
||||
for (let k = kMin; k <= kMax; k++) {
|
||||
const z = sz + gdz * k - cz;
|
||||
const R2 = x * x + y * y + z * z;
|
||||
|
||||
if (R2 > cutoffSquared) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let gaussianSum = 0;
|
||||
for (let c = 0; c < ncoeff; c++) {
|
||||
gaussianSum +=
|
||||
coefficients[c] * Math.exp(-exponents[c] * R2);
|
||||
}
|
||||
|
||||
const sphericalSum = L === 0 ? alpha[0] : sphericalFunc(alpha, x, y, z);
|
||||
|
||||
|
||||
matrix[k + oY] += gaussianSum * sphericalSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRadiusBox(grid: CubeGridInfo, center: Vec3, radius: number) {
|
||||
const r = Vec3.create(radius, radius, radius);
|
||||
const min = Vec3.scaleAndAdd(Vec3(), center, r, -1);
|
||||
const max = Vec3.add(Vec3(), center, r);
|
||||
|
||||
Vec3.sub(min, min, grid.box.min);
|
||||
Vec3.sub(max, max, grid.box.min);
|
||||
|
||||
Vec3.div(min, min, grid.delta);
|
||||
Vec3.floor(min, min);
|
||||
Vec3.max(min, min, Vec3());
|
||||
|
||||
Vec3.div(max, max, grid.delta);
|
||||
Vec3.ceil(max, max);
|
||||
Vec3.min(max, max, Vec3.subScalar(Vec3(), grid.dimensions, 1));
|
||||
|
||||
return [min, max];
|
||||
}
|
||||
131
src/extensions/alpha-orbitals/data-model.ts
Normal file
131
src/extensions/alpha-orbitals/data-model.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat4, Tensor, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Grid } from '../../mol-model/volume';
|
||||
import { SphericalBasisOrder } from './spherical-functions';
|
||||
import { Box3D, RegularGrid3d } from '../../mol-math/geometry';
|
||||
import { arrayMin, arrayMax, arrayRms, arrayMean } from '../../mol-util/array';
|
||||
|
||||
// Note: generally contracted gaussians are currently not supported.
|
||||
export interface SphericalElectronShell {
|
||||
exponents: number[];
|
||||
angularMomentum: number[];
|
||||
// number[] for each angular momentum
|
||||
coefficients: number[][];
|
||||
}
|
||||
|
||||
export interface Basis {
|
||||
atoms: {
|
||||
// in Bohr units!
|
||||
center: Vec3;
|
||||
shells: SphericalElectronShell[];
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface AlphaOrbital {
|
||||
energy: number;
|
||||
occupancy: number;
|
||||
alpha: number[];
|
||||
}
|
||||
|
||||
export interface CubeGridComputationParams {
|
||||
basis: Basis;
|
||||
/**
|
||||
* for each electron shell compute a cutoff radius as
|
||||
* const cutoffRadius = Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(exponents));
|
||||
*/
|
||||
cutoffThreshold: number;
|
||||
sphericalOrder: SphericalBasisOrder;
|
||||
boxExpand: number;
|
||||
gridSpacing: number | [atomCountThreshold: number, spacing: number][];
|
||||
doNotComputeIsovalues?: boolean;
|
||||
}
|
||||
|
||||
export interface CubeGridInfo {
|
||||
params: CubeGridComputationParams;
|
||||
dimensions: Vec3;
|
||||
box: Box3D;
|
||||
size: Vec3;
|
||||
npoints: number;
|
||||
delta: Vec3;
|
||||
}
|
||||
|
||||
export interface CubeGrid {
|
||||
grid: Grid;
|
||||
isovalues?: { negative?: number; positive?: number };
|
||||
}
|
||||
|
||||
export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
|
||||
const geometry = params.basis.atoms.map(a => a.center);
|
||||
const { gridSpacing: spacing, boxExpand: expand } = params;
|
||||
|
||||
const count = geometry.length;
|
||||
const box = Box3D.expand(
|
||||
Box3D(),
|
||||
Box3D.fromVec3Array(Box3D(), geometry),
|
||||
Vec3.create(expand, expand, expand)
|
||||
);
|
||||
const size = Box3D.size(Vec3(), box);
|
||||
|
||||
const spacingThresholds =
|
||||
typeof spacing === 'number' ? [[0, spacing]] : [...spacing];
|
||||
spacingThresholds.sort((a, b) => b[0] - a[0]);
|
||||
|
||||
let s = 0.4;
|
||||
for (let i = 0; i <= spacingThresholds.length; i++) {
|
||||
s = spacingThresholds[i][1];
|
||||
if (spacingThresholds[i][0] <= count) break;
|
||||
}
|
||||
|
||||
const dimensions = Vec3.ceil(Vec3(), Vec3.scale(Vec3(), size, 1 / s));
|
||||
|
||||
return {
|
||||
params,
|
||||
box,
|
||||
dimensions,
|
||||
size,
|
||||
npoints: dimensions[0] * dimensions[1] * dimensions[2],
|
||||
delta: Vec3.div(Vec3(), size, Vec3.subScalar(Vec3(), dimensions, 1)),
|
||||
};
|
||||
}
|
||||
|
||||
const BohrToAngstromFactor = 0.529177210859;
|
||||
|
||||
export function createGrid(gridInfo: RegularGrid3d, values: Float32Array, axisOrder: number[]) {
|
||||
const boxSize = Box3D.size(Vec3(), gridInfo.box);
|
||||
const boxOrigin = Vec3.clone(gridInfo.box.min);
|
||||
|
||||
Vec3.scale(boxSize, boxSize, BohrToAngstromFactor);
|
||||
Vec3.scale(boxOrigin, boxOrigin, BohrToAngstromFactor);
|
||||
|
||||
const scale = Mat4.fromScaling(
|
||||
Mat4(),
|
||||
Vec3.div(
|
||||
Vec3(),
|
||||
boxSize,
|
||||
Vec3.sub(Vec3(), gridInfo.dimensions, Vec3.create(1, 1, 1))
|
||||
)
|
||||
);
|
||||
const translate = Mat4.fromTranslation(Mat4(), boxOrigin);
|
||||
const matrix = Mat4.mul(Mat4(), translate, scale);
|
||||
|
||||
const grid: Grid = {
|
||||
transform: { kind: 'matrix', matrix },
|
||||
cells: Tensor.create(
|
||||
Tensor.Space(gridInfo.dimensions, axisOrder, Float32Array),
|
||||
(values as any) as Tensor.Data
|
||||
),
|
||||
stats: {
|
||||
min: arrayMin(values),
|
||||
max: arrayMax(values),
|
||||
mean: arrayMean(values),
|
||||
sigma: arrayRms(values),
|
||||
},
|
||||
};
|
||||
|
||||
return grid;
|
||||
}
|
||||
124
src/extensions/alpha-orbitals/density.ts
Normal file
124
src/extensions/alpha-orbitals/density.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { sortArray } from '../../mol-data/util';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
|
||||
import { gpuComputeAlphaOrbitalsDensityGridValues } from './gpu/compute';
|
||||
|
||||
export function createSphericalCollocationDensityGrid(
|
||||
params: CubeGridComputationParams, orbitals: AlphaOrbital[], webgl?: WebGLContext
|
||||
): Task<CubeGrid> {
|
||||
return Task.create('Spherical Collocation Grid', async (ctx) => {
|
||||
const cubeGrid = initCubeGrid(params);
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsDensityGridValues(ctx, webgl!, cubeGrid, orbitals);
|
||||
// console.timeEnd('gpu');
|
||||
} else {
|
||||
throw new Error('Missing OES_texture_float WebGL extension.');
|
||||
}
|
||||
|
||||
const grid = createGrid(cubeGrid, matrix, [0, 1, 2]);
|
||||
let isovalues: { negative?: number, positive?: number } | undefined;
|
||||
|
||||
if (!params.doNotComputeIsovalues) {
|
||||
isovalues = computeDensityIsocontourValues(matrix, 0.85);
|
||||
}
|
||||
|
||||
return { grid, isovalues };
|
||||
});
|
||||
}
|
||||
|
||||
export function computeDensityIsocontourValues(input: Float32Array, cumulativeThreshold: number) {
|
||||
let weightSum = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
weightSum += w;
|
||||
}
|
||||
const avgWeight = weightSum / input.length;
|
||||
let minWeight = 3 * avgWeight;
|
||||
|
||||
// do not try to identify isovalues for degenerate data
|
||||
// e.g. all values are almost same
|
||||
if (Math.abs(avgWeight - input[0] * input[0]) < 1e-5) {
|
||||
return { negative: void 0, positive: void 0 };
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
while (true) {
|
||||
let csum = 0;
|
||||
size = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
if (w >= minWeight) {
|
||||
csum += w;
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
if (csum / weightSum > cumulativeThreshold) {
|
||||
break;
|
||||
}
|
||||
|
||||
minWeight -= avgWeight;
|
||||
}
|
||||
|
||||
const values = new Float32Array(size);
|
||||
const weights = new Float32Array(size);
|
||||
const indices = new Int32Array(size);
|
||||
|
||||
let o = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = Math.abs(v);
|
||||
if (w >= minWeight) {
|
||||
values[o] = v;
|
||||
weights[o] = w;
|
||||
indices[o] = o;
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
sortArray(
|
||||
indices,
|
||||
(indices, i, j) => weights[indices[j]] - weights[indices[i]]
|
||||
);
|
||||
|
||||
let cweight = 0,
|
||||
cutoffIndex = 0;
|
||||
for (let i = 0; i < size; i++) {
|
||||
cweight += weights[indices[i]];
|
||||
|
||||
if (cweight / weightSum >= cumulativeThreshold) {
|
||||
cutoffIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let positive = Number.POSITIVE_INFINITY,
|
||||
negative = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let i = 0; i < cutoffIndex; i++) {
|
||||
const v = values[indices[i]];
|
||||
if (v > 0) {
|
||||
if (v < positive) positive = v;
|
||||
} else if (v < 0) {
|
||||
if (v > negative) negative = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
negative: negative !== Number.NEGATIVE_INFINITY ? negative : void 0,
|
||||
positive: positive !== Number.POSITIVE_INFINITY ? positive : void 0,
|
||||
};
|
||||
}
|
||||
170
src/extensions/alpha-orbitals/gpu/compute.ts
Normal file
170
src/extensions/alpha-orbitals/gpu/compute.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { createGrid3dComputeRenderable } from '../../../mol-gl/compute/grid3d';
|
||||
import { TextureSpec, UnboxedValues, UniformSpec } from '../../../mol-gl/renderable/schema';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { arrayMin } from '../../../mol-util/array';
|
||||
import { AlphaOrbital, Basis, CubeGridInfo } from '../data-model';
|
||||
import { normalizeBasicOrder, SphericalBasisOrder } from '../spherical-functions';
|
||||
import { MAIN, UTILS } from './shader.frag';
|
||||
|
||||
const Schema = {
|
||||
tCenters: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
|
||||
tInfo: TextureSpec('image-float32', 'rgba', 'float', 'nearest'),
|
||||
tCoeff: TextureSpec('image-float32', 'rgb', 'float', 'nearest'),
|
||||
tAlpha: TextureSpec('image-float32', 'alpha', 'float', 'nearest'),
|
||||
uNCenters: UniformSpec('i'),
|
||||
uNAlpha: UniformSpec('i'),
|
||||
uNCoeff: UniformSpec('i'),
|
||||
uMaxCoeffs: UniformSpec('i'),
|
||||
};
|
||||
|
||||
const Orbitals = createGrid3dComputeRenderable({
|
||||
schema: Schema,
|
||||
loopBounds: ['uNCenters', 'uMaxCoeffs'],
|
||||
mainCode: MAIN,
|
||||
utilCode: UTILS,
|
||||
returnCode: 'v',
|
||||
values(params: { grid: CubeGridInfo, orbital: AlphaOrbital }) {
|
||||
return createTextureData(params.grid, params.orbital);
|
||||
}
|
||||
});
|
||||
|
||||
const Density = createGrid3dComputeRenderable({
|
||||
schema: {
|
||||
...Schema,
|
||||
uOccupancy: UniformSpec('f'),
|
||||
},
|
||||
loopBounds: ['uNCenters', 'uMaxCoeffs'],
|
||||
mainCode: MAIN,
|
||||
utilCode: UTILS,
|
||||
returnCode: 'current + uOccupancy * v * v',
|
||||
values(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
|
||||
return {
|
||||
...createTextureData(params.grid, params.orbitals[0]),
|
||||
uOccupancy: 0
|
||||
};
|
||||
},
|
||||
cumulative: {
|
||||
states(params: { grid: CubeGridInfo, orbitals: AlphaOrbital[] }) {
|
||||
return params.orbitals.filter(o => o.occupancy !== 0);
|
||||
},
|
||||
update({ grid }, state: AlphaOrbital, values) {
|
||||
const alpha = getNormalizedAlpha(grid.params.basis, state.alpha, grid.params.sphericalOrder);
|
||||
ValueCell.updateIfChanged(values.uOccupancy, state.occupancy);
|
||||
ValueCell.update(values.tAlpha, { width: alpha.length, height: 1, array: alpha });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export function gpuComputeAlphaOrbitalsGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbital: AlphaOrbital) {
|
||||
return Orbitals(ctx, webgl, grid, { grid, orbital });
|
||||
}
|
||||
|
||||
export function gpuComputeAlphaOrbitalsDensityGridValues(ctx: RuntimeContext, webgl: WebGLContext, grid: CubeGridInfo, orbitals: AlphaOrbital[]) {
|
||||
return Density(ctx, webgl, grid, { grid, orbitals });
|
||||
}
|
||||
|
||||
function getNormalizedAlpha(basis: Basis, alphaOrbitals: number[], sphericalOrder: SphericalBasisOrder) {
|
||||
const alpha = new Float32Array(alphaOrbitals.length);
|
||||
|
||||
let aO = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
const a0 = normalizeBasicOrder(L, alphaOrbitals.slice(aO, aO + 2 * L + 1), sphericalOrder);
|
||||
for (let i = 0; i < a0.length; i++) alpha[aO + i] = a0[i];
|
||||
aO += 2 * L + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return alpha;
|
||||
}
|
||||
|
||||
function createTextureData(grid: CubeGridInfo, orbital: AlphaOrbital): UnboxedValues<typeof Schema> {
|
||||
const { basis, sphericalOrder, cutoffThreshold } = grid.params;
|
||||
|
||||
let centerCount = 0;
|
||||
let baseCount = 0;
|
||||
let coeffCount = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
for (const L of shell.angularMomentum) {
|
||||
if (L > 4) {
|
||||
// TODO: will L > 4 be required? Would need to precompute more functions in that case.
|
||||
throw new Error('Angular momentum L > 4 not supported.');
|
||||
}
|
||||
|
||||
centerCount++;
|
||||
baseCount += 2 * L + 1;
|
||||
coeffCount += shell.exponents.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const centers = new Float32Array(4 * centerCount);
|
||||
// L, alpha_offset, coeff_offset_start, coeff_offset_end
|
||||
const info = new Float32Array(4 * centerCount);
|
||||
const alpha = new Float32Array(baseCount);
|
||||
const coeff = new Float32Array(3 * coeffCount);
|
||||
|
||||
let maxCoeffs = 0;
|
||||
let cO = 0, aO = 0, coeffO = 0;
|
||||
for (const atom of basis.atoms) {
|
||||
for (const shell of atom.shells) {
|
||||
|
||||
let amIndex = 0;
|
||||
for (const L of shell.angularMomentum) {
|
||||
const a0 = normalizeBasicOrder(L, orbital.alpha.slice(aO, aO + 2 * L + 1), sphericalOrder);
|
||||
|
||||
const cutoffRadius = cutoffThreshold > 0
|
||||
? Math.sqrt(-Math.log(cutoffThreshold) / arrayMin(shell.exponents))
|
||||
: 10000;
|
||||
|
||||
centers[4 * cO + 0] = atom.center[0];
|
||||
centers[4 * cO + 1] = atom.center[1];
|
||||
centers[4 * cO + 2] = atom.center[2];
|
||||
centers[4 * cO + 3] = cutoffRadius * cutoffRadius;
|
||||
|
||||
info[4 * cO + 0] = L;
|
||||
info[4 * cO + 1] = aO;
|
||||
info[4 * cO + 2] = coeffO;
|
||||
info[4 * cO + 3] = coeffO + shell.exponents.length;
|
||||
|
||||
for (let i = 0; i < a0.length; i++) alpha[aO + i] = a0[i];
|
||||
|
||||
const c0 = shell.coefficients[amIndex++];
|
||||
for (let i = 0; i < shell.exponents.length; i++) {
|
||||
coeff[3 * (coeffO + i) + 0] = c0[i];
|
||||
coeff[3 * (coeffO + i) + 1] = shell.exponents[i];
|
||||
}
|
||||
|
||||
if (c0.length > maxCoeffs) {
|
||||
maxCoeffs = c0.length;
|
||||
}
|
||||
|
||||
cO++;
|
||||
aO += 2 * L + 1;
|
||||
coeffO += shell.exponents.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
uNCenters: centerCount,
|
||||
uNAlpha: baseCount,
|
||||
uNCoeff: coeffCount,
|
||||
uMaxCoeffs: maxCoeffs,
|
||||
tCenters: { width: centerCount, height: 1, array: centers },
|
||||
tInfo: { width: centerCount, height: 1, array: info },
|
||||
tCoeff: { width: coeffCount, height: 1, array: coeff },
|
||||
tAlpha: { width: baseCount, height: 1, array: alpha },
|
||||
};
|
||||
}
|
||||
145
src/extensions/alpha-orbitals/gpu/shader.frag.ts
Normal file
145
src/extensions/alpha-orbitals/gpu/shader.frag.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const UTILS = `
|
||||
float L1(vec3 p, float a0, float a1, float a2) {
|
||||
return a0 * p.z + a1 * p.x + a2 * p.y;
|
||||
}
|
||||
|
||||
float L2(vec3 p, float a0, float a1, float a2, float a3, float a4) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
return (
|
||||
a0 * (-0.5 * xx - 0.5 * yy + zz) +
|
||||
a1 * (1.7320508075688772 * x * z) +
|
||||
a2 * (1.7320508075688772 * y * z) +
|
||||
a3 * (0.8660254037844386 * xx - 0.8660254037844386 * yy) +
|
||||
a4 * (1.7320508075688772 * x * y)
|
||||
);
|
||||
}
|
||||
|
||||
float L3(vec3 p, float a0, float a1, float a2, float a3, float a4, float a5, float a6) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
float xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
return (
|
||||
a0 * (-1.5 * xx * z - 1.5 * yy * z + zzz) +
|
||||
a1 * (-0.6123724356957945 * xxx - 0.6123724356957945 * x * yy + 2.449489742783178 * x * zz) +
|
||||
a2 * (-0.6123724356957945 * xx * y - 0.6123724356957945 * yyy + 2.449489742783178 * y * zz) +
|
||||
a3 * (1.9364916731037085 * xx * z - 1.9364916731037085 * yy * z) +
|
||||
a4 * (3.872983346207417 * x * y * z) +
|
||||
a5 * (0.7905694150420949 * xxx - 2.3717082451262845 * x * yy) +
|
||||
a6 * (2.3717082451262845 * xx * y - 0.7905694150420949 * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
float L4(vec3 p, float a0, float a1, float a2, float a3, float a4, float a5, float a6, float a7, float a8) {
|
||||
float x = p.x, y = p.y, z = p.z;
|
||||
float xx = x * x, yy = y * y, zz = z * z;
|
||||
float xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
float xxxx = xxx * x, yyyy = yyy * y, zzzz = zzz * z;
|
||||
return (
|
||||
a0 * (0.375 * xxxx + 0.75 * xx * yy + 0.375 * yyyy - 3.0 * xx * zz - 3.0 * yy * zz + zzzz) +
|
||||
a1 * (-2.3717082451262845 * xxx * z - 2.3717082451262845 * x * yy * z + 3.1622776601683795 * x * zzz) +
|
||||
a2 * (-2.3717082451262845 * xx * y * z - 2.3717082451262845 * yyy * z + 3.1622776601683795 * y * zzz) +
|
||||
a3 * (-0.5590169943749475 * xxxx + 0.5590169943749475 * yyyy + 3.3541019662496847 * xx * zz - 3.3541019662496847 * yy * zz) +
|
||||
a4 * (-1.118033988749895 * xxx * y - 1.118033988749895 * x * yyy + 6.708203932499369 * x * y * zz) +
|
||||
a5 * (2.091650066335189 * xxx * z + -6.274950199005566 * x * yy * z) +
|
||||
a6 * (6.274950199005566 * xx * y * z + -2.091650066335189 * yyy * z) +
|
||||
a7 * (0.739509972887452 * xxxx - 4.437059837324712 * xx * yy + 0.739509972887452 * yyyy) +
|
||||
a8 * (2.958039891549808 * xxx * y + -2.958039891549808 * x * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
float alpha(float offset, float f) {
|
||||
#ifdef WEBGL1
|
||||
// in webgl1, the value is in the alpha channel!
|
||||
return texture2D(tAlpha, vec2(offset * f, 0.5)).a;
|
||||
#else
|
||||
return texture2D(tAlpha, vec2(offset * f, 0.5)).x;
|
||||
#endif
|
||||
}
|
||||
|
||||
float Y(int L, vec3 X, float aO, float fA) {
|
||||
if (L == 0) {
|
||||
return alpha(aO, fA);
|
||||
} else if (L == 1) {
|
||||
return L1(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA)
|
||||
);
|
||||
} else if (L == 2) {
|
||||
return L2(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA)
|
||||
);
|
||||
} else if (L == 3) {
|
||||
return L3(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA),
|
||||
alpha(aO + 5.0, fA), alpha(aO + 6.0, fA)
|
||||
);
|
||||
} else if (L == 4) {
|
||||
return L4(X,
|
||||
alpha(aO, fA), alpha(aO + 1.0, fA), alpha(aO + 2.0, fA), alpha(aO + 3.0, fA), alpha(aO + 4.0, fA),
|
||||
alpha(aO + 5.0, fA), alpha(aO + 6.0, fA), alpha(aO + 7.0, fA), alpha(aO + 8.0, fA)
|
||||
);
|
||||
}
|
||||
// TODO: do we need L > 4?
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
#ifndef WEBGL1
|
||||
float R(float R2, int start, int end, float fCoeff) {
|
||||
float gauss = 0.0;
|
||||
for (int i = start; i < end; i++) {
|
||||
vec2 c = texture2D(tCoeff, vec2(float(i) * fCoeff, 0.5)).xy;
|
||||
gauss += c.x * exp(-c.y * R2);
|
||||
}
|
||||
return gauss;
|
||||
}
|
||||
#else
|
||||
float R(float R2, int start, int end, float fCoeff) {
|
||||
float gauss = 0.0;
|
||||
int o = start;
|
||||
for (int i = 0; i < uMaxCoeffs; i++) {
|
||||
if (o >= end) break;
|
||||
|
||||
vec2 c = texture2D(tCoeff, vec2(float(o) * fCoeff, 0.5)).xy;
|
||||
gauss += c.x * exp(-c.y * R2);
|
||||
o++;
|
||||
}
|
||||
return gauss;
|
||||
}
|
||||
#endif
|
||||
`;
|
||||
|
||||
export const MAIN = `
|
||||
float fCenter = 1.0 / float(uNCenters - 1);
|
||||
float fCoeff = 1.0 / float(uNCoeff - 1);
|
||||
float fA = 1.0 / float(uNAlpha - 1);
|
||||
|
||||
float v = 0.0;
|
||||
|
||||
for (int i = 0; i < uNCenters; i++) {
|
||||
vec2 cCoord = vec2(float(i) * fCenter, 0.5);
|
||||
|
||||
vec4 center = texture2D(tCenters, cCoord);
|
||||
vec3 X = xyz - center.xyz;
|
||||
float R2 = dot(X, X);
|
||||
|
||||
// center.w is squared cutoff radius
|
||||
if (R2 > center.w) {
|
||||
continue;
|
||||
}
|
||||
|
||||
vec4 info = texture2D(tInfo, cCoord);
|
||||
|
||||
int L = int(info.x);
|
||||
float aO = info.y;
|
||||
int coeffStart = int(info.z);
|
||||
int coeffEnd = int(info.w);
|
||||
|
||||
v += R(R2, coeffStart, coeffEnd, fCoeff) * Y(L, X, aO, fA);
|
||||
}
|
||||
`;
|
||||
131
src/extensions/alpha-orbitals/orbitals.ts
Normal file
131
src/extensions/alpha-orbitals/orbitals.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { sortArray } from '../../mol-data/util';
|
||||
import { canComputeGrid3dOnGPU } from '../../mol-gl/compute/grid3d';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { sphericalCollocation } from './collocation';
|
||||
import { AlphaOrbital, createGrid, CubeGrid, CubeGridComputationParams, initCubeGrid } from './data-model';
|
||||
import { gpuComputeAlphaOrbitalsGridValues } from './gpu/compute';
|
||||
|
||||
// setDebugMode(true);
|
||||
|
||||
export function createSphericalCollocationGrid(
|
||||
params: CubeGridComputationParams, orbital: AlphaOrbital, webgl?: WebGLContext
|
||||
): Task<CubeGrid> {
|
||||
return Task.create('Spherical Collocation Grid', async (ctx) => {
|
||||
const cubeGrid = initCubeGrid(params);
|
||||
|
||||
let matrix: Float32Array;
|
||||
if (canComputeGrid3dOnGPU(webgl)) {
|
||||
// console.time('gpu');
|
||||
matrix = await gpuComputeAlphaOrbitalsGridValues(ctx, webgl!, cubeGrid, orbital);
|
||||
// console.timeEnd('gpu');
|
||||
} else {
|
||||
// console.time('cpu');
|
||||
matrix = await sphericalCollocation(cubeGrid, orbital, ctx);
|
||||
// console.timeEnd('cpu');
|
||||
}
|
||||
|
||||
const grid = createGrid(cubeGrid, matrix, [0, 1, 2]);
|
||||
let isovalues: { negative?: number, positive?: number } | undefined;
|
||||
|
||||
if (!params.doNotComputeIsovalues) {
|
||||
isovalues = computeOrbitalIsocontourValues(matrix, 0.85);
|
||||
}
|
||||
|
||||
return { grid, isovalues };
|
||||
});
|
||||
}
|
||||
|
||||
export function computeOrbitalIsocontourValues(input: Float32Array, cumulativeThreshold: number) {
|
||||
let weightSum = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
weightSum += w;
|
||||
}
|
||||
const avgWeight = weightSum / input.length;
|
||||
let minWeight = 3 * avgWeight;
|
||||
|
||||
// do not try to identify isovalues for degenerate data
|
||||
// e.g. all values are almost same
|
||||
if (Math.abs(avgWeight - input[0] * input[0]) < 1e-5) {
|
||||
return { negative: void 0, positive: void 0 };
|
||||
}
|
||||
|
||||
let size = 0;
|
||||
while (true) {
|
||||
let csum = 0;
|
||||
size = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
if (w >= minWeight) {
|
||||
csum += w;
|
||||
size++;
|
||||
}
|
||||
}
|
||||
|
||||
if (csum / weightSum > cumulativeThreshold) {
|
||||
break;
|
||||
}
|
||||
|
||||
minWeight -= avgWeight;
|
||||
}
|
||||
|
||||
const values = new Float32Array(size);
|
||||
const weights = new Float32Array(size);
|
||||
const indices = new Int32Array(size);
|
||||
|
||||
let o = 0;
|
||||
for (let i = 0, _i = input.length; i < _i; i++) {
|
||||
const v = input[i];
|
||||
const w = v * v;
|
||||
if (w >= minWeight) {
|
||||
values[o] = v;
|
||||
weights[o] = w;
|
||||
indices[o] = o;
|
||||
o++;
|
||||
}
|
||||
}
|
||||
|
||||
sortArray(
|
||||
indices,
|
||||
(indices, i, j) => weights[indices[j]] - weights[indices[i]]
|
||||
);
|
||||
|
||||
let cweight = 0,
|
||||
cutoffIndex = 0;
|
||||
for (let i = 0; i < size; i++) {
|
||||
cweight += weights[indices[i]];
|
||||
|
||||
if (cweight / weightSum >= cumulativeThreshold) {
|
||||
cutoffIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let positive = Number.POSITIVE_INFINITY,
|
||||
negative = Number.NEGATIVE_INFINITY;
|
||||
|
||||
for (let i = 0; i < cutoffIndex; i++) {
|
||||
const v = values[indices[i]];
|
||||
if (v > 0) {
|
||||
if (v < positive) positive = v;
|
||||
} else if (v < 0) {
|
||||
if (v > negative) negative = v;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
negative: negative !== Number.NEGATIVE_INFINITY ? negative : void 0,
|
||||
positive: positive !== Number.POSITIVE_INFINITY ? positive : void 0,
|
||||
};
|
||||
}
|
||||
93
src/extensions/alpha-orbitals/spherical-functions.ts
Normal file
93
src/extensions/alpha-orbitals/spherical-functions.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Inspired by https://github.com/dgasmith/gau2grid.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
// gaussian:
|
||||
// R_0, R^+_1, R^-_1, ..., R^+_l, R^-_l
|
||||
// cca:
|
||||
// R^-_(l), R^-_(l-1), ..., R_0, ..., R^+_(l-1), R^+_l
|
||||
// cca-reverse:
|
||||
// R^+_(l), R^+_(l-1), ..., R_0, ..., R^-_(l-1), R^-_l
|
||||
export type SphericalBasisOrder = 'gaussian' | 'cca' | 'cca-reverse';
|
||||
|
||||
export function normalizeBasicOrder(
|
||||
L: number,
|
||||
alpha: number[],
|
||||
order: SphericalBasisOrder
|
||||
) {
|
||||
if (order === 'gaussian' || L === 0) return alpha;
|
||||
|
||||
const ret: number[] = [alpha[L]];
|
||||
for (let l = 0; l < L; l++) {
|
||||
const a = alpha[L - l - 1],
|
||||
b = alpha[L + l + 1];
|
||||
if (order === 'cca') ret.push(b, a);
|
||||
else ret.push(a, b);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
export type SphericalFunc = (
|
||||
alpha: number[],
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
) => number;
|
||||
|
||||
export const SphericalFunctions: SphericalFunc[] = [L0, L1, L2, L3, L4];
|
||||
|
||||
// L_i functions were auto-generated.
|
||||
|
||||
function L0(alpha: number[], x: number, y: number, z: number) {
|
||||
return alpha[0];
|
||||
}
|
||||
|
||||
function L1(alpha: number[], x: number, y: number, z: number) {
|
||||
return alpha[0] * z + alpha[1] * x + alpha[2] * y;
|
||||
}
|
||||
|
||||
function L2(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
return (
|
||||
alpha[0] * (-0.5 * xx - 0.5 * yy + zz) +
|
||||
alpha[1] * (1.7320508075688772 * x * z) +
|
||||
alpha[2] * (1.7320508075688772 * y * z) +
|
||||
alpha[3] * (0.8660254037844386 * xx - 0.8660254037844386 * yy) +
|
||||
alpha[4] * (1.7320508075688772 * x * y)
|
||||
);
|
||||
}
|
||||
|
||||
function L3(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
const xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
return (
|
||||
alpha[0] * (-1.5 * xx * z - 1.5 * yy * z + zzz) +
|
||||
alpha[1] * (-0.6123724356957945 * xxx - 0.6123724356957945 * x * yy + 2.449489742783178 * x * zz) +
|
||||
alpha[2] * (-0.6123724356957945 * xx * y - 0.6123724356957945 * yyy + 2.449489742783178 * y * zz) +
|
||||
alpha[3] * (1.9364916731037085 * xx * z - 1.9364916731037085 * yy * z) +
|
||||
alpha[4] * (3.872983346207417 * x * y * z) +
|
||||
alpha[5] * (0.7905694150420949 * xxx - 2.3717082451262845 * x * yy) +
|
||||
alpha[6] * (2.3717082451262845 * xx * y - 0.7905694150420949 * yyy)
|
||||
);
|
||||
}
|
||||
|
||||
function L4(alpha: number[], x: number, y: number, z: number) {
|
||||
const xx = x * x, yy = y * y, zz = z * z;
|
||||
const xxx = xx * x, yyy = yy * y, zzz = zz * z;
|
||||
const xxxx = xxx * x, yyyy = yyy * y, zzzz = zzz * z;
|
||||
return (
|
||||
alpha[0] * (0.375 * xxxx + 0.75 * xx * yy + 0.375 * yyyy - 3.0 * xx * zz - 3.0 * yy * zz + zzzz) +
|
||||
alpha[1] * (-2.3717082451262845 * xxx * z - 2.3717082451262845 * x * yy * z + 3.1622776601683795 * x * zzz) +
|
||||
alpha[2] * (-2.3717082451262845 * xx * y * z - 2.3717082451262845 * yyy * z + 3.1622776601683795 * y * zzz) +
|
||||
alpha[3] * (-0.5590169943749475 * xxxx + 0.5590169943749475 * yyyy + 3.3541019662496847 * xx * zz - 3.3541019662496847 * yy * zz) +
|
||||
alpha[4] * (-1.118033988749895 * xxx * y - 1.118033988749895 * x * yyy + 6.708203932499369 * x * y * zz) +
|
||||
alpha[5] * (2.091650066335189 * xxx * z + -6.274950199005566 * x * yy * z) +
|
||||
alpha[6] * (6.274950199005566 * xx * y * z + -2.091650066335189 * yyy * z) +
|
||||
alpha[7] * (0.739509972887452 * xxxx - 4.437059837324712 * xx * yy + 0.739509972887452 * yyyy) +
|
||||
alpha[8] * (2.958039891549808 * xxx * y + -2.958039891549808 * x * yyy)
|
||||
);
|
||||
}
|
||||
193
src/extensions/alpha-orbitals/transforms.ts
Normal file
193
src/extensions/alpha-orbitals/transforms.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { createSphericalCollocationGrid } from './orbitals';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Task } from '../../mol-task';
|
||||
import { CustomProperties } from '../../mol-model/custom-property';
|
||||
import { SphericalBasisOrder } from './spherical-functions';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
|
||||
import { AlphaOrbital, Basis, CubeGrid } from './data-model';
|
||||
import { createSphericalCollocationDensityGrid } from './density';
|
||||
|
||||
export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }
|
||||
|
||||
export const StaticBasisAndOrbitals = PluginStateTransform.BuiltIn({
|
||||
name: 'static-basis-and-orbitals',
|
||||
display: 'Basis and Orbitals',
|
||||
from: PluginStateObject.Root,
|
||||
to: BasisAndOrbitals,
|
||||
params: {
|
||||
label: PD.Text('Orbital Data', { isHidden: true }),
|
||||
basis: PD.Value<Basis>(void 0 as any, { isHidden: true }),
|
||||
order: PD.Text<SphericalBasisOrder>('gaussian' as SphericalBasisOrder, { isHidden: true }),
|
||||
orbitals: PD.Value<AlphaOrbital[]>([], { isHidden: true })
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new BasisAndOrbitals({ basis: params.basis, order: params.order, orbitals: params.orbitals }, { label: params.label });
|
||||
}
|
||||
});
|
||||
|
||||
const CreateOrbitalVolumeParamBase = {
|
||||
cutoffThreshold: PD.Numeric(0.0015, { min: 0, max: 0.1, step: 0.0001 }),
|
||||
boxExpand: PD.Numeric(4.5, { min: 0, max: 7, step: 0.1 }),
|
||||
gridSpacing: PD.ObjectList({ atomCount: PD.Numeric(0), spacing: PD.Numeric(0.35, { min: 0.1, max: 2, step: 0.01 }) }, e => `Atoms ${e.atomCount}: ${e.spacing}`, {
|
||||
defaultValue: [
|
||||
{ atomCount: 55, spacing: 0.5 },
|
||||
{ atomCount: 40, spacing: 0.45 },
|
||||
{ atomCount: 25, spacing: 0.4 },
|
||||
{ atomCount: 0, spacing: 0.35 },
|
||||
]
|
||||
})
|
||||
};
|
||||
|
||||
export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-volume',
|
||||
display: 'Orbital Volume',
|
||||
from: BasisAndOrbitals,
|
||||
to: PluginStateObject.Volume.Data,
|
||||
params: (a) => {
|
||||
if (!a) {
|
||||
return { index: PD.Numeric(0), ...CreateOrbitalVolumeParamBase };
|
||||
}
|
||||
|
||||
return {
|
||||
index: PD.Select(0, a.data.orbitals.map((o, i) => [i, `[${i + 1}] ${o.energy.toFixed(4)}`])),
|
||||
...CreateOrbitalVolumeParamBase
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Orbital Volume', async ctx => {
|
||||
const data = await createSphericalCollocationGrid({
|
||||
basis: a.data.basis,
|
||||
cutoffThreshold: params.cutoffThreshold,
|
||||
sphericalOrder: a.data.order,
|
||||
boxExpand: params.boxExpand,
|
||||
gridSpacing: params.gridSpacing.map(e => [e.atomCount, e.spacing] as [number, number])
|
||||
}, a.data.orbitals[params.index], plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
};
|
||||
|
||||
return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-density-volume',
|
||||
display: 'Orbital Density Volume',
|
||||
from: BasisAndOrbitals,
|
||||
to: PluginStateObject.Volume.Data,
|
||||
params: CreateOrbitalVolumeParamBase
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Orbital Volume', async ctx => {
|
||||
const data = await createSphericalCollocationDensityGrid({
|
||||
basis: a.data.basis,
|
||||
cutoffThreshold: params.cutoffThreshold,
|
||||
sphericalOrder: a.data.order,
|
||||
boxExpand: params.boxExpand,
|
||||
gridSpacing: params.gridSpacing.map(e => [e.atomCount, e.spacing] as [number, number])
|
||||
}, a.data.orbitals, plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
sourceData: { name: 'custom grid', kind: 'alpha-orbitals', data },
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
};
|
||||
|
||||
return new PluginStateObject.Volume.Data(volume, { label: 'Orbital Volume' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
name: 'create-orbital-representation-3d',
|
||||
display: 'Orbital Representation 3D',
|
||||
from: PluginStateObject.Volume.Data,
|
||||
to: PluginStateObject.Volume.Representation3D,
|
||||
params: {
|
||||
directVolume: PD.Boolean(false),
|
||||
relativeIsovalue: PD.Numeric(1, { min: 0.01, max: 5, step: 0.01 }),
|
||||
kind: PD.Select<'positive' | 'negative'>('positive', [['positive', 'Positive'], ['negative', 'Negative']]),
|
||||
color: PD.Color(ColorNames.blue),
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
xrayShaded: PD.Boolean(false),
|
||||
pickable: PD.Boolean(true)
|
||||
}
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params: srcParams }, plugin: PluginContext) {
|
||||
return Task.create('Orbitals Representation 3D', async ctx => {
|
||||
const params = volumeParams(plugin, a, srcParams);
|
||||
|
||||
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
|
||||
const provider = plugin.representation.volume.registry.get(params.type.name);
|
||||
if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
|
||||
const props = params.type.params || {};
|
||||
const repr = provider.factory({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.volume.themes }, provider.getParams);
|
||||
repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, params));
|
||||
await repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
repr.setState({ pickable: srcParams.pickable });
|
||||
return new PluginStateObject.Volume.Representation3D({ repr, source: a }, { label: provider.label, description: VolumeRepresentation3DHelpers.getDescription(props) });
|
||||
});
|
||||
},
|
||||
update({ a, b, newParams: srcParams }, plugin: PluginContext) {
|
||||
return Task.create('Orbitals Representation 3D', async ctx => {
|
||||
const newParams = volumeParams(plugin, a, srcParams);
|
||||
|
||||
const props = { ...b.data.repr.props, ...newParams.type.params };
|
||||
b.data.repr.setTheme(Theme.create(plugin.representation.volume.themes, { volume: a.data }, newParams));
|
||||
await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
b.data.repr.setState({ pickable: srcParams.pickable });
|
||||
b.description = VolumeRepresentation3DHelpers.getDescription(props);
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function volumeParams(plugin: PluginContext, volume: PluginStateObject.Volume.Data, params: StateTransformer.Params<typeof CreateOrbitalRepresentation3D>) {
|
||||
if (volume.data.sourceData.kind !== 'alpha-orbitals') throw new Error('Invalid data source kind.');
|
||||
|
||||
const { isovalues } = volume.data.sourceData.data as CubeGrid;
|
||||
if (!isovalues) throw new Error('Isovalues are not computed.');
|
||||
|
||||
const value = isovalues[params.kind];
|
||||
|
||||
return createVolumeRepresentationParams(plugin, volume.data, params.directVolume ? {
|
||||
type: 'direct-volume',
|
||||
typeParams: {
|
||||
alpha: params.alpha,
|
||||
renderMode: {
|
||||
name: 'isosurface',
|
||||
params: { isoValue: { kind: 'absolute', absoluteValue: (value ?? 1000) * params.relativeIsovalue }, singleLayer: false }
|
||||
},
|
||||
xrayShaded: params.xrayShaded
|
||||
},
|
||||
color: 'uniform',
|
||||
colorParams: { value: params.color }
|
||||
} : {
|
||||
type: 'isosurface',
|
||||
typeParams: { isoValue: { kind: 'absolute', absoluteValue: (value ?? 1000) * params.relativeIsovalue }, alpha: params.alpha, xrayShaded: params.xrayShaded },
|
||||
color: 'uniform',
|
||||
colorParams: { value: params.color }
|
||||
});
|
||||
}
|
||||
@@ -106,7 +106,7 @@ function getBilayerRims(ctx: RuntimeContext, data: Structure, props: BilayerRims
|
||||
const builder = LinesBuilder.create(128, 64, shape?.geometry);
|
||||
getLayerCircle(builder, p1, centroid, normal, scaledRadius, props);
|
||||
getLayerCircle(builder, p2, centroid, normal, scaledRadius, props);
|
||||
return Shape.create(name, data, builder.getLines(), () => props.color, () => props.linesSize, () => membraneLabel(data));
|
||||
return Shape.create('Bilayer rims', data, builder.getLines(), () => props.color, () => props.linesSize, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerCircle(builder: LinesBuilder, p: Vec3, centroid: Vec3, normal: Vec3, radius: number, props: BilayerRimsProps, shape?: Shape<Lines>) {
|
||||
@@ -142,7 +142,7 @@ function getBilayerPlanes(ctx: RuntimeContext, data: Structure, props: BilayerPl
|
||||
const scaledRadius = props.radiusFactor * radius;
|
||||
getLayerPlane(state, p1, centroid, normal, scaledRadius);
|
||||
getLayerPlane(state, p2, centroid, normal, scaledRadius);
|
||||
return Shape.create(name, data, MeshBuilder.getMesh(state), () => props.color, () => 1, () => membraneLabel(data));
|
||||
return Shape.create('Bilayer planes', data, MeshBuilder.getMesh(state), () => props.color, () => 1, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerPlane(state: MeshBuilder.State, p: Vec3, centroid: Vec3, normal: Vec3, radius: number) {
|
||||
@@ -160,7 +160,7 @@ function getBilayerSpheres(ctx: RuntimeContext, data: Structure, props: BilayerS
|
||||
const spheresBuilder = SpheresBuilder.create(256, 128, shape?.geometry);
|
||||
getLayerSpheres(spheresBuilder, planePoint1, normalVector, density, scaledRadius);
|
||||
getLayerSpheres(spheresBuilder, planePoint2, normalVector, density, scaledRadius);
|
||||
return Shape.create(name, data, spheresBuilder.getSpheres(), () => props.color, () => props.sphereSize, () => membraneLabel(data));
|
||||
return Shape.create('Bilayer spheres', data, spheresBuilder.getSpheres(), () => props.color, () => props.sphereSize, () => membraneLabel(data));
|
||||
}
|
||||
|
||||
function getLayerSpheres(spheresBuilder: SpheresBuilder, point: Vec3, normalVector: Vec3, density: number, sqRadius: number) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { PluginStateObject as PSO } from '../../mol-plugin-state/objects';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Ingredient, IngredientSource, CellPacking } from './data';
|
||||
import { getFromPdb, getFromCellPackDB, IngredientFiles, parseCif, parsePDBfile, getStructureMean, getFromOPM } from './util';
|
||||
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit } from '../../mol-model/structure';
|
||||
import { Model, Structure, StructureSymmetry, StructureSelection, QueryContext, Unit, Trajectory } from '../../mol-model/structure';
|
||||
import { trajectoryFromMmCIF, MmcifFormat } from '../../mol-model-formats/structure/mmcif';
|
||||
import { trajectoryFromPDB } from '../../mol-model-formats/structure/pdb';
|
||||
import { Mat4, Vec3, Quat } from '../../mol-math/linear-algebra';
|
||||
@@ -36,8 +36,8 @@ function getCellPackModelUrl(fileName: string, baseUrl: string) {
|
||||
}
|
||||
|
||||
class TrajectoryCache {
|
||||
private map = new Map<string, Model.Trajectory>();
|
||||
set(id: string, trajectory: Model.Trajectory) { this.map.set(id, trajectory); }
|
||||
private map = new Map<string, Trajectory>();
|
||||
set(id: string, trajectory: Trajectory) { this.map.set(id, trajectory); }
|
||||
get(id: string) { return this.map.get(id); }
|
||||
}
|
||||
|
||||
@@ -94,9 +94,9 @@ async function getModel(plugin: PluginContext, id: string, ingredient: Ingredien
|
||||
trajectory = await plugin.runTask(trajectoryFromMmCIF(data.mmcif));
|
||||
}
|
||||
}
|
||||
trajCache.set(id, trajectory);
|
||||
trajCache.set(id, trajectory!);
|
||||
}
|
||||
const model = trajectory[modelIndex];
|
||||
const model = await plugin.resolveTask(trajectory?.getFrameAtIndex(modelIndex)!);
|
||||
return { model, assets };
|
||||
}
|
||||
|
||||
@@ -303,7 +303,7 @@ async function getCurve(plugin: PluginContext, name: string, ingredient: Ingredi
|
||||
const curveModelTask = Task.create('Curve Model', async ctx => {
|
||||
const format = MmcifFormat.fromFrame(cif);
|
||||
const models = await createModels(format.data.db, format, ctx);
|
||||
return models[0];
|
||||
return models.representative;
|
||||
});
|
||||
|
||||
const curveModel = await plugin.runTask(curveModelTask);
|
||||
@@ -442,9 +442,9 @@ async function handleHivRna(plugin: PluginContext, packings: CellPacking[], base
|
||||
|
||||
async function loadMembrane(plugin: PluginContext, name: string, state: State, params: LoadCellPackModelParams) {
|
||||
let file: Asset.File | undefined = undefined;
|
||||
if (params.ingredients.files !== null) {
|
||||
if (params.ingredients !== null) {
|
||||
const fileName = `${name}.bcif`;
|
||||
for (const f of params.ingredients.files) {
|
||||
for (const f of params.ingredients) {
|
||||
if (fileName === f.name) {
|
||||
file = f;
|
||||
break;
|
||||
@@ -453,7 +453,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
if (!file){
|
||||
// check for cif directly
|
||||
const cifileName = `${name}.cif`;
|
||||
for (const f of params.ingredients.files) {
|
||||
for (const f of params.ingredients) {
|
||||
if (cifileName === f.name) {
|
||||
file = f;
|
||||
break;
|
||||
@@ -488,7 +488,7 @@ async function loadMembrane(plugin: PluginContext, name: string, state: State, p
|
||||
}
|
||||
|
||||
async function loadPackings(plugin: PluginContext, runtime: RuntimeContext, state: State, params: LoadCellPackModelParams) {
|
||||
const ingredientFiles = params.ingredients.files || [];
|
||||
const ingredientFiles = params.ingredients || [];
|
||||
|
||||
let cellPackJson: StateBuilder.To<PSO.Format.Json, StateTransformer<PSO.Data.String, PSO.Format.Json>>;
|
||||
if (params.source.name === 'id') {
|
||||
@@ -563,9 +563,7 @@ const LoadCellPackModelParams = {
|
||||
}, { options: [['id', 'Id'], ['file', 'File']] }),
|
||||
baseUrl: PD.Text(DefaultCellPackBaseUrl),
|
||||
membrane: PD.Boolean(true),
|
||||
ingredients : PD.Group({
|
||||
files: PD.FileList({ accept: '.cif,.bcif,.pdb' })
|
||||
}, { isExpanded: true }),
|
||||
ingredients: PD.FileList({ accept: '.cif,.bcif,.pdb', label: 'Ingredients' }),
|
||||
preset: PD.Group({
|
||||
traceOnly: PD.Boolean(false),
|
||||
representation: PD.Select('gaussian-surface', PD.arrayToOptions(['spacefill', 'gaussian-surface', 'point', 'orientation']))
|
||||
|
||||
@@ -125,7 +125,7 @@ const StructureFromAssemblies = PluginStateTransform.BuiltIn({
|
||||
const s = await StructureSymmetry.buildAssembly(initial_structure, a.id).runInContext(ctx);
|
||||
structures.push(s);
|
||||
}
|
||||
const builder = Structure.Builder({ label: name });
|
||||
const builder = Structure.Builder();
|
||||
let offsetInvariantId = 0;
|
||||
for (const s of structures) {
|
||||
let maxInvariantId = 0;
|
||||
|
||||
@@ -55,7 +55,7 @@ function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationI
|
||||
|
||||
const prop = ConfalPyramidsProvider.get(structure.model).value;
|
||||
if (prop === undefined || prop.data === undefined) {
|
||||
return LocationIterator(0, 1, () => NullLocation);
|
||||
return LocationIterator(0, 1, 1, () => NullLocation);
|
||||
}
|
||||
|
||||
const { locations } = prop.data;
|
||||
@@ -64,7 +64,7 @@ function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationI
|
||||
if (locations.length <= groupIndex) return NullLocation;
|
||||
return locations[groupIndex];
|
||||
};
|
||||
return LocationIterator(locations.length, instanceCount, getLocation);
|
||||
return LocationIterator(locations.length, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
|
||||
|
||||
66
src/extensions/g3d/data.ts
Normal file
66
src/extensions/g3d/data.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import msgpackDecode from '../../mol-io/common/msgpack/decode';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { inflate } from '../../mol-util/zip/zip';
|
||||
|
||||
export interface G3dHeader {
|
||||
magic: 'G3D',
|
||||
version: number,
|
||||
genome: string,
|
||||
name: string,
|
||||
offsets: { [resolution: string]: { offset: number, size: number } },
|
||||
resolutions: number[]
|
||||
}
|
||||
|
||||
export type G3dDataBlock = {
|
||||
header: G3dHeader,
|
||||
resolution: number,
|
||||
data: {
|
||||
[haplotype: string]: {
|
||||
[ch: string]: {
|
||||
start: number[]
|
||||
x: number[],
|
||||
y: number[],
|
||||
z: number[],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const HEADER_SIZE = 64000;
|
||||
|
||||
export async function getG3dHeader(ctx: PluginContext, urlOrData: string | Uint8Array): Promise<G3dHeader> {
|
||||
const data: Uint8Array = await getRawData(ctx, urlOrData, { offset: 0, size: HEADER_SIZE });
|
||||
let last = data.length - 1;
|
||||
for (; last >= 0; last--) {
|
||||
if (data[last] !== 0) break;
|
||||
}
|
||||
const header = msgpackDecode(data.slice(0, last + 1));
|
||||
return header;
|
||||
}
|
||||
|
||||
export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, urlOrData: string | Uint8Array, resolution: number): Promise<G3dDataBlock> {
|
||||
if (!header.offsets[resolution]) throw new Error(`Resolution ${resolution} not available.`);
|
||||
const data = await getRawData(ctx, urlOrData, header.offsets[resolution]);
|
||||
const unzipped = await ctx.runTask(Task.create('Unzip', ctx => inflate(ctx, data)));
|
||||
|
||||
return {
|
||||
header,
|
||||
resolution,
|
||||
data: msgpackDecode(unzipped)
|
||||
};
|
||||
}
|
||||
|
||||
async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
|
||||
if (typeof urlOrData === 'string') {
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
|
||||
} else {
|
||||
return urlOrData.slice(range.offset, range.offset + range.size);
|
||||
}
|
||||
}
|
||||
173
src/extensions/g3d/format.ts
Normal file
173
src/extensions/g3d/format.ts
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Trajectory } from '../../mol-model/structure';
|
||||
import { TrajectoryFormatCategory, TrajectoryFormatProvider } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject as SO, PluginStateTransform } from '../../mol-plugin-state/objects';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/base';
|
||||
import { StateAction, StateObjectRef } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { G3dHeader, getG3dDataBlock, getG3dHeader } from './data';
|
||||
import { g3dHaplotypeQuery, G3dLabelProvider, trajectoryFromG3D, G3dSymbols, G3dInfoDataProperty } from './model';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { objectForEach } from '../../mol-util/object';
|
||||
|
||||
export const G3dProvider: TrajectoryFormatProvider = {
|
||||
label: 'G3D',
|
||||
description: 'G3D',
|
||||
category: TrajectoryFormatCategory,
|
||||
binaryExtensions: ['g3d'],
|
||||
parse: async (plugin, data) => {
|
||||
const trajectory = await plugin.state.data.build()
|
||||
.to(data)
|
||||
.apply(G3DHeaderFromFile, {}, { state: { isGhost: true } })
|
||||
.apply(G3DTrajectory)
|
||||
.commit();
|
||||
|
||||
return { trajectory };
|
||||
},
|
||||
visuals: defaultStructure
|
||||
};
|
||||
|
||||
async function defaultStructure(plugin: PluginContext, data: { trajectory: StateObjectRef<SO.Molecule.Trajectory> }) {
|
||||
const builder = plugin.builders.structure;
|
||||
const model = await builder.createModel(data.trajectory);
|
||||
|
||||
if (!model) return;
|
||||
const structure = await builder.createStructure(model);
|
||||
|
||||
const info = G3dInfoDataProperty.get(model.data!);
|
||||
if (!info) return;
|
||||
|
||||
const components = plugin.build().to(structure);
|
||||
|
||||
const repr = createStructureRepresentationParams(plugin, void 0, {
|
||||
type: 'cartoon',
|
||||
color: 'polymer-index',
|
||||
size: 'uniform',
|
||||
sizeParams: { value: 0.25 }
|
||||
});
|
||||
|
||||
for (const h of info.haplotypes) {
|
||||
components
|
||||
.apply(StateTransforms.Model.StructureSelectionFromExpression, { expression: g3dHaplotypeQuery(h), label: stringToWords(h) })
|
||||
.apply(StateTransforms.Representation.StructureRepresentation3D, repr);
|
||||
}
|
||||
|
||||
await components.commit();
|
||||
}
|
||||
|
||||
export class G3dHeaderObject extends SO.Create<{
|
||||
header: G3dHeader,
|
||||
urlOrData: Uint8Array | string,
|
||||
cache: { [resolution: number]: Trajectory | undefined }
|
||||
}>({ name: 'G3D Header', typeClass: 'Data' }) { }
|
||||
|
||||
export type G3DHeaderFromFile = typeof G3DHeaderFromFile
|
||||
export const G3DHeaderFromFile = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-header-from-file',
|
||||
display: { name: 'G3D Header', description: 'Parse G3D Header' },
|
||||
from: SO.Data.Binary,
|
||||
to: G3dHeaderObject
|
||||
})({
|
||||
apply({ a }, plugin: PluginContext) {
|
||||
return Task.create('Parse G3D', async () => {
|
||||
const header = await getG3dHeader(plugin, a.data);
|
||||
return new G3dHeaderObject({ header, urlOrData: a.data, cache: { } }, { label: header.name, description: header.genome });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type G3DHeaderFromUrl = typeof G3DHeaderFromUrl
|
||||
export const G3DHeaderFromUrl = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-header-from-url',
|
||||
display: { name: 'G3D Header', description: 'Parse G3D Header' },
|
||||
params: { url: ParamDefinition.Text('') },
|
||||
from: SO.Root,
|
||||
to: G3dHeaderObject
|
||||
})({
|
||||
apply({ params }, plugin: PluginContext) {
|
||||
return Task.create('Parse G3D', async () => {
|
||||
const header = await getG3dHeader(plugin, params.url);
|
||||
return new G3dHeaderObject({ header, urlOrData: params.url, cache: { } }, { label: header.name, description: header.genome });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export type G3DTrajectory = typeof G3DHeaderFromUrl
|
||||
export const G3DTrajectory = PluginStateTransform.BuiltIn({
|
||||
name: 'g3d-trajecotry',
|
||||
display: { name: 'G3D Trajectory', description: 'Create G3D Trajectory' },
|
||||
params: a => {
|
||||
if (!a) return { resolution: ParamDefinition.Numeric(200000) };
|
||||
const rs = a.data.header.resolutions;
|
||||
return {
|
||||
resolution: ParamDefinition.Select(rs[rs.length - 1], rs.map(r => [r, '' + r] as const))
|
||||
};
|
||||
},
|
||||
from: G3dHeaderObject,
|
||||
to: SO.Molecule.Trajectory
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('G3D Trajectory', async ctx => {
|
||||
if (a.data.cache[params.resolution]) {
|
||||
return new SO.Molecule.Trajectory(a.data.cache[params.resolution]!, { label: a.label, description: a.description });
|
||||
}
|
||||
const data = await getG3dDataBlock(plugin, a.data.header, a.data.urlOrData, params.resolution);
|
||||
const traj = await trajectoryFromG3D(data).runInContext(ctx);
|
||||
a.data.cache[params.resolution] = traj;
|
||||
return new SO.Molecule.Trajectory(traj, { label: a.label, description: a.description });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const LoadG3D = StateAction.build({
|
||||
display: { name: 'Load Genome 3D (G3D)', description: 'Load G3D file from the specified URL.' },
|
||||
from: SO.Root,
|
||||
params: { url: ParamDefinition.Text('') }
|
||||
})(({ params, state }, ctx: PluginContext) => Task.create('Genome3D', taskCtx => {
|
||||
return state.transaction(async () => {
|
||||
if (params.url.trim().length === 0) {
|
||||
throw new Error('Specify URL');
|
||||
}
|
||||
|
||||
ctx.behaviors.layout.leftPanelTabName.next('data');
|
||||
|
||||
const trajectory = await state.build().toRoot()
|
||||
.apply(G3DHeaderFromUrl, { url: params.url })
|
||||
.apply(G3DTrajectory)
|
||||
.commit();
|
||||
|
||||
await defaultStructure(ctx, { trajectory });
|
||||
}).runInContext(taskCtx);
|
||||
}));
|
||||
|
||||
export const G3DFormat = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
|
||||
name: 'g3d',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'G3D',
|
||||
description: 'G3D Format Support'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
|
||||
register() {
|
||||
this.ctx.state.data.actions.add(LoadG3D);
|
||||
objectForEach(G3dSymbols, s => DefaultQueryRuntimeTable.addSymbol(s));
|
||||
this.ctx.managers.lociLabels.addProvider(G3dLabelProvider);
|
||||
}
|
||||
unregister() {
|
||||
this.ctx.state.data.actions.remove(LoadG3D);
|
||||
objectForEach(G3dSymbols, s => DefaultQueryRuntimeTable.removeSymbol(s));
|
||||
this.ctx.managers.lociLabels.removeProvider(G3dLabelProvider);
|
||||
}
|
||||
}
|
||||
});
|
||||
245
src/extensions/g3d/model.ts
Normal file
245
src/extensions/g3d/model.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../mol-data/db';
|
||||
import { OrderedSet } from '../../mol-data/int';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { createModels } from '../../mol-model-formats/structure/basic/parser';
|
||||
import { BasicSchema, createBasic } from '../../mol-model-formats/structure/basic/schema';
|
||||
import { EntityBuilder } from '../../mol-model-formats/structure/common/entity';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
import { Trajectory, Unit } from '../../mol-model/structure';
|
||||
import { MoleculeType } from '../../mol-model/structure/model/types';
|
||||
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { MolScriptBuilder as MS } from '../../mol-script/language/builder';
|
||||
import { CustomPropSymbol } from '../../mol-script/language/symbol';
|
||||
import Type from '../../mol-script/language/type';
|
||||
import { QuerySymbolRuntime } from '../../mol-script/runtime/query/base';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { objectForEach } from '../../mol-util/object';
|
||||
import { G3dDataBlock } from './data';
|
||||
import { FormatPropertyProvider } from '../../mol-model-formats/structure/common/property';
|
||||
|
||||
interface NormalizedData {
|
||||
entity_id: string[],
|
||||
chromosome: string[],
|
||||
seq_id_begin: Int32Array,
|
||||
seq_id_end: Int32Array,
|
||||
start: Int32Array,
|
||||
x: Float32Array,
|
||||
y: Float32Array,
|
||||
z: Float32Array,
|
||||
r: Float32Array,
|
||||
haplotype: string[]
|
||||
}
|
||||
|
||||
function getColumns(block: G3dDataBlock) {
|
||||
const { data } = block;
|
||||
let size = 0;
|
||||
|
||||
objectForEach(data, h => objectForEach(h, g => size += g.start.length));
|
||||
|
||||
const normalized: NormalizedData = {
|
||||
entity_id: new Array(size),
|
||||
chromosome: new Array(size),
|
||||
seq_id_begin: new Int32Array(size),
|
||||
seq_id_end: new Int32Array(size),
|
||||
start: new Int32Array(size),
|
||||
x: new Float32Array(size),
|
||||
y: new Float32Array(size),
|
||||
z: new Float32Array(size),
|
||||
r: new Float32Array(size),
|
||||
haplotype: new Array(size)
|
||||
};
|
||||
|
||||
const p = [Vec3(), Vec3(), Vec3()];
|
||||
|
||||
let o = 0;
|
||||
objectForEach(data, (hs, h) => {
|
||||
objectForEach(hs, (chs, ch) => {
|
||||
const entity_id = `${ch}-${h}`;
|
||||
const l = chs.start.length;
|
||||
if (l === 0) return;
|
||||
|
||||
let x = chs.x[0];
|
||||
let y = chs.y[0];
|
||||
let z = chs.z[0];
|
||||
|
||||
Vec3.set(p[0], x, y, z);
|
||||
Vec3.set(p[2], x, y, z);
|
||||
|
||||
for (let i = 0; i < l; i++) {
|
||||
normalized.entity_id[o] = entity_id;
|
||||
normalized.chromosome[o] = ch;
|
||||
normalized.start[o] = chs.start[i];
|
||||
normalized.seq_id_begin[o] = o;
|
||||
normalized.seq_id_end[o] = o;
|
||||
|
||||
x = chs.x[i];
|
||||
y = chs.y[i];
|
||||
z = chs.z[i];
|
||||
|
||||
Vec3.set(p[1], x, y, z);
|
||||
if (i + 1 < l) Vec3.set(p[2], chs.x[i + 1], chs.y[i + 1], chs.z[i + 1]);
|
||||
else Vec3.set(p[2], x, y, z);
|
||||
|
||||
normalized.x[o] = x;
|
||||
normalized.y[o] = y;
|
||||
normalized.z[o] = z;
|
||||
normalized.r[o] = 2 / 3 * Math.min(Vec3.distance(p[0], p[1]), Vec3.distance(p[1], p[2]));
|
||||
normalized.haplotype[o] = h;
|
||||
|
||||
const _p = p[0];
|
||||
p[0] = p[1];
|
||||
p[1] = _p;
|
||||
o++;
|
||||
}
|
||||
|
||||
if (l === 1) {
|
||||
normalized.r[o - 1] = 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
async function getTraj(ctx: RuntimeContext, data: G3dDataBlock) {
|
||||
const normalized = getColumns(data);
|
||||
|
||||
const rowCount = normalized.seq_id_begin.length;
|
||||
const entityIds = new Array<string>(rowCount);
|
||||
const entityBuilder = new EntityBuilder();
|
||||
|
||||
const eName = { customName: '' };
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const e = normalized.entity_id[i];
|
||||
eName.customName = e;
|
||||
const entityId = entityBuilder.getEntityId(e, MoleculeType.DNA, e, eName);
|
||||
entityIds[i] = entityId;
|
||||
}
|
||||
|
||||
const ihm_sphere_obj_site = Table.ofPartialColumns(BasicSchema.ihm_sphere_obj_site, {
|
||||
id: Column.range(0, rowCount),
|
||||
entity_id: Column.ofStringArray(entityIds),
|
||||
seq_id_begin: Column.ofIntArray(normalized.seq_id_begin),
|
||||
seq_id_end: Column.ofIntArray(normalized.seq_id_end),
|
||||
asym_id: Column.ofStringArray(normalized.chromosome),
|
||||
|
||||
Cartn_x: Column.ofFloatArray(normalized.x),
|
||||
Cartn_y: Column.ofFloatArray(normalized.y),
|
||||
Cartn_z: Column.ofFloatArray(normalized.z),
|
||||
|
||||
object_radius: Column.ofFloatArray(normalized.r),
|
||||
rmsf: Column.ofConst(0, rowCount, Column.Schema.float),
|
||||
model_id: Column.ofConst(1, rowCount, Column.Schema.int),
|
||||
}, rowCount);
|
||||
|
||||
const basic = createBasic({
|
||||
entity: entityBuilder.getEntityTable(),
|
||||
ihm_model_list: Table.ofPartialColumns(BasicSchema.ihm_model_list, {
|
||||
model_id: Column.ofIntArray([1]),
|
||||
model_name: Column.ofStringArray(['G3D Model']),
|
||||
}, 1),
|
||||
ihm_sphere_obj_site
|
||||
});
|
||||
|
||||
const models = await createModels(basic, { kind: 'g3d', name: 'G3D', data }, ctx);
|
||||
|
||||
G3dInfoDataProperty.set(models.representative, {
|
||||
haplotypes: Object.keys(data.data),
|
||||
haplotype: normalized.haplotype,
|
||||
resolution: data.resolution,
|
||||
start: normalized.start,
|
||||
chroms: normalized.chromosome,
|
||||
});
|
||||
|
||||
return models;
|
||||
}
|
||||
|
||||
export function trajectoryFromG3D(data: G3dDataBlock): Task<Trajectory> {
|
||||
return Task.create('Parse G3D', async ctx => {
|
||||
return getTraj(ctx, data);
|
||||
});
|
||||
}
|
||||
|
||||
export const G3dSymbols = {
|
||||
haplotype: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'haplotype', Type.Str),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const info = (G3dInfoDataProperty as any).get(ctx.element.unit.model);
|
||||
if (!info) return '';
|
||||
const seqId = ctx.element.unit.model.coarseHierarchy.spheres.seq_id_begin.value(ctx.element.element);
|
||||
return info.haplotype[seqId] || '';
|
||||
}
|
||||
),
|
||||
chromosome: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'chromosome', Type.Str),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const { asym_id } = ctx.element.unit.model.coarseHierarchy.spheres;
|
||||
return asym_id.value(ctx.element.element) || '';
|
||||
}
|
||||
),
|
||||
region: QuerySymbolRuntime.Dynamic(CustomPropSymbol('g3d', 'region', Type.Num),
|
||||
ctx => {
|
||||
if (Unit.isAtomic(ctx.element.unit)) return '';
|
||||
const info = (G3dInfoDataProperty as any).get(ctx.element.unit.model);
|
||||
if (!info) return 0;
|
||||
const seqId = ctx.element.unit.model.coarseHierarchy.spheres.seq_id_begin.value(ctx.element.element);
|
||||
return info.start[seqId] || 0;
|
||||
}
|
||||
)
|
||||
};
|
||||
|
||||
export const G3dInfoDataProperty = FormatPropertyProvider.create<G3dInfoData>({ name: 'g3d_info' });
|
||||
|
||||
export function g3dHaplotypeQuery(haplotype: string) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.rel.eq([G3dSymbols.haplotype.symbol(), haplotype]),
|
||||
});
|
||||
}
|
||||
|
||||
export function g3dChromosomeQuery(chr: string) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.logic.and([
|
||||
MS.core.rel.eq([MS.ammp('objectPrimitive'), 'sphere']),
|
||||
MS.core.rel.eq([G3dSymbols.chromosome.symbol(), chr])
|
||||
])
|
||||
});
|
||||
}
|
||||
|
||||
export function g3dRegionQuery(chr: string, start: number, end: number) {
|
||||
return MS.struct.generator.atomGroups({
|
||||
'chain-test': MS.core.logic.and([
|
||||
MS.core.rel.eq([MS.ammp('objectPrimitive'), 'sphere']),
|
||||
MS.core.rel.eq([G3dSymbols.chromosome.symbol(), chr])
|
||||
]),
|
||||
'residue-test': MS.core.rel.inRange([G3dSymbols.region.symbol(), start, end])
|
||||
});
|
||||
}
|
||||
|
||||
export interface G3dInfoData {
|
||||
haplotypes: string[],
|
||||
haplotype: string[],
|
||||
start: Int32Array,
|
||||
resolution: number,
|
||||
chroms: string[]
|
||||
};
|
||||
|
||||
export const G3dLabelProvider: LociLabelProvider = {
|
||||
label: (e: Loci): string | undefined => {
|
||||
if (e.kind !== 'element-loci' || Loci.isEmpty(e)) return;
|
||||
|
||||
const first = e.elements[0];
|
||||
if (e.elements.length !== 1 || Unit.isAtomic(first.unit)) return;
|
||||
const info = G3dInfoDataProperty.get(first.unit.model);
|
||||
if (!info) return;
|
||||
|
||||
const eI = first.unit.elements[OrderedSet.getAt(first.indices, 0)];
|
||||
const seqId = first.unit.model.coarseHierarchy.spheres.seq_id_begin.value(eI);
|
||||
return `<b>Start:</b> ${info.start[seqId]} <small>| resolution ${info.resolution}<small>`;
|
||||
}
|
||||
};
|
||||
144
src/extensions/mp4-export/controls.ts
Normal file
144
src/extensions/mp4-export/controls.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
|
||||
import { PluginComponent } from '../../mol-plugin-state/component';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { encodeMp4Animation } from './encoder';
|
||||
|
||||
export interface Mp4AnimationInfo {
|
||||
width: number,
|
||||
height: number
|
||||
}
|
||||
|
||||
export const Mp4AnimationParams = {
|
||||
quantization: PD.Numeric(18, { min: 10, max: 51 }, { description: 'Lower is better, but slower.' })
|
||||
};
|
||||
|
||||
export class Mp4Controls extends PluginComponent {
|
||||
private currentNames = new Set<string>();
|
||||
private animations: PluginStateAnimation[] = [];
|
||||
|
||||
readonly behaviors = {
|
||||
animations: this.ev.behavior<PD.Params>({ }),
|
||||
current: this.ev.behavior<{ anim: PluginStateAnimation, params: PD.Params, values: any } | undefined>(void 0),
|
||||
canApply: this.ev.behavior<PluginStateAnimation.CanApply>({ canApply: false }),
|
||||
info: this.ev.behavior<Mp4AnimationInfo>({ width: 0, height: 0 }),
|
||||
params: this.ev.behavior<PD.Values<typeof Mp4AnimationParams>>(PD.getDefaultValues(Mp4AnimationParams))
|
||||
}
|
||||
|
||||
setCurrent(name?: string) {
|
||||
const anim = this.animations.find(a => a.name === name);
|
||||
if (!anim) {
|
||||
this.behaviors.current.next(anim);
|
||||
return;
|
||||
}
|
||||
|
||||
const params = anim.params(this.plugin) as PD.Params;
|
||||
const values = PD.getDefaultValues(params);
|
||||
|
||||
this.behaviors.current.next({ anim, params, values });
|
||||
this.behaviors.canApply.next(anim.canApply?.(this.plugin) ?? { canApply: true });
|
||||
}
|
||||
|
||||
setCurrentParams(values: any) {
|
||||
this.behaviors.current.next({ ...this.behaviors.current.value!, values });
|
||||
}
|
||||
|
||||
get current() {
|
||||
return this.behaviors.current.value;
|
||||
}
|
||||
|
||||
render() {
|
||||
const task = Task.create('Export Animation', async ctx => {
|
||||
try {
|
||||
const resolution = this.plugin.helpers.viewportScreenshot?.getSizeAndViewport()!;
|
||||
const anim = this.current!;
|
||||
const movie = await encodeMp4Animation(this.plugin, ctx, {
|
||||
animation: {
|
||||
definition: anim.anim,
|
||||
params: anim.values,
|
||||
},
|
||||
...resolution,
|
||||
quantizationParameter: this.behaviors.params.value.quantization,
|
||||
pass: this.plugin.helpers.viewportScreenshot?.imagePass!,
|
||||
});
|
||||
|
||||
const filename = anim.anim.display.name.toLowerCase().replace(/\s/g, '-').replace(/[^a-z0-9_\-]/g, '');
|
||||
return { movie, filename: `${this.plugin.helpers.viewportScreenshot?.getFilename('')}_${filename}.mp4` };
|
||||
} catch (e) {
|
||||
this.plugin.log.error('' + e);
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
|
||||
return this.plugin.runTask(task, { useOverlay: true });
|
||||
}
|
||||
|
||||
private get manager() {
|
||||
return this.plugin.managers.animation;
|
||||
}
|
||||
|
||||
private syncInfo() {
|
||||
const helper = this.plugin.helpers.viewportScreenshot;
|
||||
const size = helper?.getSizeAndViewport();
|
||||
if (!size) return;
|
||||
|
||||
this.behaviors.info.next({ width: size.viewport.width, height: size.viewport.height });
|
||||
}
|
||||
|
||||
private sync() {
|
||||
const animations = this.manager.animations.filter(a => a.isExportable);
|
||||
|
||||
const hasAll = animations.every(a => this.currentNames.has(a.name));
|
||||
if (hasAll && this.currentNames.size === animations.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const params = {
|
||||
current: PD.Select(animations[0]?.name,
|
||||
animations.map(a => [a.name, a.display.name] as [string, string]),
|
||||
{ label: 'Animation' })
|
||||
};
|
||||
|
||||
const current = this.behaviors.current.value;
|
||||
const hasCurrent = !!animations.find(a => a.name === current?.anim.name);
|
||||
|
||||
this.animations = animations;
|
||||
if (!hasCurrent) {
|
||||
this.setCurrent(animations[0]?.name);
|
||||
}
|
||||
this.behaviors.animations.next(params);
|
||||
}
|
||||
|
||||
private init() {
|
||||
this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
|
||||
this.sync();
|
||||
});
|
||||
|
||||
this.subscribe(this.plugin.canvas3d?.resized!, () => this.syncInfo());
|
||||
this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
|
||||
|
||||
this.subscribe(this.plugin.behaviors.state.isBusy, b => {
|
||||
const anim = this.current;
|
||||
if (!b && anim) {
|
||||
this.behaviors.canApply.next(anim.anim.canApply?.(this.plugin) ?? { canApply: true });
|
||||
}
|
||||
});
|
||||
|
||||
this.sync();
|
||||
this.syncInfo();
|
||||
}
|
||||
|
||||
constructor(private plugin: PluginContext) {
|
||||
super();
|
||||
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
105
src/extensions/mp4-export/encoder.ts
Normal file
105
src/extensions/mp4-export/encoder.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as HME from 'h264-mp4-encoder';
|
||||
import { Viewport } from '../../mol-canvas3d/camera/util';
|
||||
import { ImagePass } from '../../mol-canvas3d/passes/image';
|
||||
import { PluginStateAnimation } from '../../mol-plugin-state/animation/model';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAnimation> {
|
||||
pass: ImagePass,
|
||||
customBackground?: Color,
|
||||
animation: PluginStateAnimation.Instance<A>,
|
||||
width: number,
|
||||
height: number,
|
||||
viewport: Viewport,
|
||||
/** default is 30 */
|
||||
fps?: number,
|
||||
/** Number from 10 (best quality, slowest) to 51 (worst, fastest) */
|
||||
quantizationParameter?: number
|
||||
}
|
||||
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
|
||||
await ctx.update({ message: 'Initializing...', isIndeterminate: true });
|
||||
|
||||
validateViewport(params);
|
||||
const durationMs = PluginStateAnimation.getDuration(plugin, params.animation);
|
||||
if (durationMs === void 0) {
|
||||
throw new Error('The animation does not have the duration specified.');
|
||||
}
|
||||
|
||||
const encoder = await HME.createH264MP4Encoder();
|
||||
|
||||
const { width, height } = params;
|
||||
let vw = params.viewport.width, vh = params.viewport.height;
|
||||
|
||||
// dimensions must be a multiple of 2
|
||||
if (vw % 2 !== 0) vw -= 1;
|
||||
if (vh % 2 !== 0) vh -= 1;
|
||||
|
||||
const normalizedViewport: Viewport = { ...params.viewport, width: vw, height: vh };
|
||||
|
||||
encoder.width = vw;
|
||||
encoder.height = vh;
|
||||
if (params.quantizationParameter) encoder.quantizationParameter = params.quantizationParameter;
|
||||
if (params.fps) encoder.frameRate = params.fps;
|
||||
encoder.initialize();
|
||||
|
||||
const loop = plugin.animationLoop;
|
||||
const canvasProps = plugin.canvas3d?.props;
|
||||
const wasAnimating = loop.isAnimating;
|
||||
let stoppedAnimation = true, finalized = false;
|
||||
|
||||
try {
|
||||
loop.stop();
|
||||
loop.resetTime(0);
|
||||
|
||||
if (params.customBackground !== void 0) {
|
||||
plugin.canvas3d?.setProps({ renderer: { backgroundColor: params.customBackground }, transparentBackground: false }, true);
|
||||
}
|
||||
|
||||
const fps = encoder.frameRate;
|
||||
const N = Math.ceil(durationMs / 1000 * fps);
|
||||
const dt = durationMs / N;
|
||||
|
||||
await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
|
||||
|
||||
await plugin.managers.animation.play(params.animation.definition, params.animation.params);
|
||||
stoppedAnimation = false;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
await loop.tick(i * dt, { isSynchronous: true, manualDraw: true });
|
||||
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ current: i + 1 });
|
||||
}
|
||||
}
|
||||
await ctx.update({ message: 'Applying finishing touches...', isIndeterminate: true });
|
||||
await plugin.managers.animation.stop();
|
||||
stoppedAnimation = true;
|
||||
encoder.finalize();
|
||||
finalized = true;
|
||||
return encoder.FS.readFile(encoder.outputFilename);
|
||||
} finally {
|
||||
if (finalized) encoder.delete();
|
||||
if (params.customBackground !== void 0) {
|
||||
plugin.canvas3d?.setProps({ renderer: { backgroundColor: canvasProps?.renderer!.backgroundColor }, transparentBackground: canvasProps?.transparentBackground });
|
||||
}
|
||||
if (!stoppedAnimation) await plugin.managers.animation.stop();
|
||||
if (wasAnimating) loop.start();
|
||||
}
|
||||
}
|
||||
|
||||
function validateViewport(params: Mp4EncoderParams) {
|
||||
if (params.viewport.x + params.viewport.width > params.width || params.viewport.y + params.viewport.height > params.height) {
|
||||
throw new Error('Viewport exceeds the canvas dimensions.');
|
||||
}
|
||||
}
|
||||
30
src/extensions/mp4-export/index.ts
Normal file
30
src/extensions/mp4-export/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { Mp4EncoderUI } from './ui';
|
||||
|
||||
export const Mp4Export = PluginBehavior.create<{ }>({
|
||||
name: 'extension-mp4-export',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'MP4 Animation Export'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.customStructureControls.set('mp4-export', Mp4EncoderUI as any);
|
||||
}
|
||||
|
||||
update() {
|
||||
return false;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customStructureControls.delete('mp4-export');
|
||||
}
|
||||
},
|
||||
params: () => ({ })
|
||||
});
|
||||
123
src/extensions/mp4-export/ui.tsx
Normal file
123
src/extensions/mp4-export/ui.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { merge } from 'rxjs';
|
||||
import { debounceTime } from 'rxjs/operators';
|
||||
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
|
||||
import { Button } from '../../mol-plugin-ui/controls/common';
|
||||
import { CameraOutlinedSvg, GetAppSvg, Icon, SubscriptionsOutlinedSvg } from '../../mol-plugin-ui/controls/icons';
|
||||
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { Mp4AnimationParams, Mp4Controls } from './controls';
|
||||
|
||||
interface State {
|
||||
busy?: boolean,
|
||||
data?: { movie: Uint8Array, filename: string };
|
||||
}
|
||||
|
||||
export class Mp4EncoderUI extends CollapsableControls<{}, State> {
|
||||
private _controls: Mp4Controls | undefined;
|
||||
|
||||
get controls() {
|
||||
return this._controls || (this._controls = new Mp4Controls(this.plugin));
|
||||
}
|
||||
|
||||
protected defaultState(): State & CollapsableState {
|
||||
return {
|
||||
header: 'Export Animation',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: SubscriptionsOutlinedSvg }
|
||||
};
|
||||
}
|
||||
|
||||
private downloadControls() {
|
||||
return <>
|
||||
<div className='msp-control-offset msp-help-text'>
|
||||
<div className='msp-help-description' style={{ textAlign: 'center' }}>
|
||||
Rendering successful!
|
||||
</div>
|
||||
</div>
|
||||
<Button icon={GetAppSvg} onClick={this.save} style={{ marginTop: 1 }}>Save Animation</Button>
|
||||
<Button onClick={() => this.setState({ data: void 0 })} style={{ marginTop: 6 }}>Clear</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
if (this.state.data) {
|
||||
return this.downloadControls();
|
||||
}
|
||||
|
||||
const ctrl = this.controls;
|
||||
const current = ctrl.behaviors.current.value;
|
||||
const info = ctrl.behaviors.info.value;
|
||||
const canApply = ctrl.behaviors.canApply.value;
|
||||
return <>
|
||||
<ParameterControls
|
||||
params={ctrl.behaviors.animations.value}
|
||||
values={{ current: current?.anim.name }}
|
||||
onChangeValues={xs => ctrl.setCurrent(xs.current)}
|
||||
isDisabled={this.state.busy}
|
||||
/>
|
||||
{current && <ParameterControls
|
||||
params={current.params}
|
||||
values={current.values}
|
||||
onChangeValues={xs => ctrl.setCurrentParams(xs)}
|
||||
isDisabled={this.state.busy}
|
||||
/>}
|
||||
<div className='msp-control-offset msp-help-text'>
|
||||
<div className='msp-help-description' style={{ textAlign: 'center' }}>
|
||||
Resolution: {info.width}x{info.height}<br />
|
||||
Adjust in viewport using <Icon svg={CameraOutlinedSvg} inline />
|
||||
</div>
|
||||
</div>
|
||||
<ParameterControls
|
||||
params={Mp4AnimationParams}
|
||||
values={ctrl.behaviors.params.value}
|
||||
onChangeValues={xs => ctrl.behaviors.params.next(xs)}
|
||||
isDisabled={this.state.busy}
|
||||
/>
|
||||
<Button onClick={this.generate} style={{ marginTop: 1 }}
|
||||
disabled={this.state.busy || !canApply.canApply}
|
||||
commit={canApply.canApply ? 'on' : 'off'}>
|
||||
{canApply.canApply ? 'Render' : canApply.reason ?? 'Invalid params/state'}
|
||||
</Button>
|
||||
</>;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const merged = merge(
|
||||
this.controls.behaviors.animations,
|
||||
this.controls.behaviors.current,
|
||||
this.controls.behaviors.canApply,
|
||||
this.controls.behaviors.info,
|
||||
this.controls.behaviors.params
|
||||
);
|
||||
|
||||
this.subscribe(merged.pipe(debounceTime(10)), () => {
|
||||
if (!this.state.isCollapsed) this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this._controls?.dispose();
|
||||
this._controls = void 0;
|
||||
}
|
||||
|
||||
save = () => {
|
||||
download(new Blob([this.state.data!.movie]), this.state.data!.filename);
|
||||
}
|
||||
|
||||
generate = async () => {
|
||||
try {
|
||||
this.setState({ busy: true });
|
||||
const data = await this.controls.render();
|
||||
this.setState({ busy: false, data });
|
||||
} catch {
|
||||
this.setState({ busy: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@ const ColorLegend = TableLegend([
|
||||
]);
|
||||
|
||||
export function getGeometricQualityColorThemeParams(ctx: ThemeDataContext) {
|
||||
const validationReport = ctx.structure && ValidationReportProvider.get(ctx.structure.models[0]).value;
|
||||
const validationReport = !!ctx.structure && ctx.structure.models.length > 0 && ValidationReportProvider.get(ctx.structure.models[0]).value;
|
||||
const options: [string, string][] = [];
|
||||
if (validationReport) {
|
||||
const kinds = new Set<string>();
|
||||
@@ -48,7 +48,7 @@ export type GeometricQualityColorThemeParams = ReturnType<typeof getGeometricQua
|
||||
export function GeometryQualityColorTheme(ctx: ThemeDataContext, props: PD.Values<GeometricQualityColorThemeParams>): ColorTheme<GeometricQualityColorThemeParams> {
|
||||
let color: LocationColor = () => DefaultColor;
|
||||
|
||||
const validationReport = ctx.structure && ValidationReportProvider.get(ctx.structure.models[0]);
|
||||
const validationReport = !!ctx.structure && ctx.structure.models.length > 0 ? ValidationReportProvider.get(ctx.structure.models[0]) : void 0;
|
||||
const contextHash = validationReport?.version;
|
||||
|
||||
const value = validationReport?.value;
|
||||
|
||||
@@ -187,7 +187,7 @@ type InterUnitClashesProps = {
|
||||
}
|
||||
|
||||
export type IntraUnitClashes = IntAdjacencyGraph<UnitIndex, IntraUnitClashesProps>
|
||||
export type InterUnitClashes = InterUnitGraph<Unit.Atomic, UnitIndex, InterUnitClashesProps>
|
||||
export type InterUnitClashes = InterUnitGraph<number, UnitIndex, InterUnitClashesProps>
|
||||
|
||||
export interface Clashes {
|
||||
readonly interUnit: InterUnitClashes
|
||||
@@ -195,7 +195,7 @@ export interface Clashes {
|
||||
}
|
||||
|
||||
function createInterUnitClashes(structure: Structure, clashes: ValidationReport['clashes']) {
|
||||
const builder = new InterUnitGraph.Builder<Unit.Atomic, UnitIndex, InterUnitClashesProps>();
|
||||
const builder = new InterUnitGraph.Builder<number, UnitIndex, InterUnitClashesProps>();
|
||||
const { a, b, edgeProps: { id, magnitude, distance } } = clashes;
|
||||
|
||||
const pA = Vec3(), pB = Vec3();
|
||||
@@ -204,7 +204,7 @@ function createInterUnitClashes(structure: Structure, clashes: ValidationReport[
|
||||
const elementsA = unitA.elements;
|
||||
const elementsB = unitB.elements;
|
||||
|
||||
builder.startUnitPair(unitA as Unit.Atomic, unitB as Unit.Atomic);
|
||||
builder.startUnitPair(unitA.id, unitB.id);
|
||||
|
||||
for (let i = 0, il = clashes.edgeCount * 2; i < il; ++i) {
|
||||
// TODO create lookup
|
||||
|
||||
@@ -143,7 +143,7 @@ function createIntraClashIterator(structureGroup: StructureGroup): LocationItera
|
||||
location.element = unit.elements[a[groupIndex]];
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, getLocation);
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -159,7 +159,8 @@ function createInterUnitClashCylinderMesh(ctx: VisualContext, structure: Structu
|
||||
linkCount: edgeCount,
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const b = edges[edgeIndex];
|
||||
const uA = b.unitA, uB = b.unitB;
|
||||
const uA = structure.unitMap.get(b.unitA);
|
||||
const uB = structure.unitMap.get(b.unitB);
|
||||
uA.conformation.position(uA.elements[b.indexA], posA);
|
||||
uB.conformation.position(uB.elements[b.indexB], posB);
|
||||
},
|
||||
@@ -197,11 +198,13 @@ export function InterUnitClashVisual(materialId: number): ComplexVisual<InterUni
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function getInterClashBoundingSphere(clashes: InterUnitClashes, elements: number[], boundingSphere: Sphere3D) {
|
||||
function getInterClashBoundingSphere(structure: Structure, clashes: InterUnitClashes, elements: number[], boundingSphere: Sphere3D) {
|
||||
return CentroidHelper.fromPairProvider(elements.length, (i, pA, pB) => {
|
||||
const c = clashes.edges[elements[i]];
|
||||
c.unitA.conformation.position(c.unitA.elements[c.indexA], pA);
|
||||
c.unitB.conformation.position(c.unitB.elements[c.indexB], pB);
|
||||
const uA = structure.unitMap.get(c.unitA);
|
||||
const uB = structure.unitMap.get(c.unitB);
|
||||
uA.conformation.position(uA.elements[c.indexA], pA);
|
||||
uB.conformation.position(uB.elements[c.indexB], pB);
|
||||
}, boundingSphere);
|
||||
}
|
||||
|
||||
@@ -209,18 +212,20 @@ function getInterClashLabel(structure: Structure, clashes: InterUnitClashes, ele
|
||||
const idx = elements[0];
|
||||
if (idx === undefined) return '';
|
||||
const c = clashes.edges[idx];
|
||||
const uA = structure.unitMap.get(c.unitA);
|
||||
const uB = structure.unitMap.get(c.unitB);
|
||||
const mag = c.props.magnitude.toFixed(2);
|
||||
const dist = c.props.distance.toFixed(2);
|
||||
|
||||
return [
|
||||
`Clash id: ${c.props.id} | Magnitude: ${mag} \u212B | Distance: ${dist} \u212B`,
|
||||
bondLabel(Bond.Location(structure, c.unitA, c.indexA, structure, c.unitB, c.indexB))
|
||||
bondLabel(Bond.Location(structure, uA, c.indexA, structure, uB, c.indexB))
|
||||
].join('</br>');
|
||||
}
|
||||
|
||||
function InterClashLoci(structure: Structure, clashes: InterUnitClashes, elements: number[]) {
|
||||
return DataLoci('inter-clashes', clashes, elements,
|
||||
(boundingSphere: Sphere3D) => getInterClashBoundingSphere(clashes, elements, boundingSphere),
|
||||
(boundingSphere: Sphere3D) => getInterClashBoundingSphere(structure, clashes, elements, boundingSphere),
|
||||
() => getInterClashLabel(structure, clashes, elements));
|
||||
}
|
||||
|
||||
@@ -246,11 +251,11 @@ function createInterClashIterator(structure: Structure): LocationIterator {
|
||||
const location = StructureElement.Location.create(structure);
|
||||
const getLocation = (groupIndex: number) => {
|
||||
const clash = clashes.edges[groupIndex];
|
||||
location.unit = clash.unitA;
|
||||
location.element = clash.unitA.elements[clash.indexA];
|
||||
location.unit = structure.unitMap.get(clash.unitA);
|
||||
location.element = location.unit.elements[clash.indexA];
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, getLocation, true);
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation, true);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -10,22 +10,37 @@ import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
export { Camera };
|
||||
export { ICamera, Camera };
|
||||
|
||||
class Camera {
|
||||
interface ICamera {
|
||||
readonly viewport: Viewport,
|
||||
readonly view: Mat4,
|
||||
readonly projection: Mat4,
|
||||
readonly projectionView: Mat4,
|
||||
readonly inverseProjectionView: Mat4,
|
||||
readonly state: Readonly<Camera.Snapshot>,
|
||||
readonly viewOffset: Camera.ViewOffset,
|
||||
readonly far: number,
|
||||
readonly near: number,
|
||||
readonly fogFar: number,
|
||||
readonly fogNear: number,
|
||||
}
|
||||
|
||||
class Camera implements ICamera {
|
||||
readonly view: Mat4 = Mat4.identity();
|
||||
readonly projection: Mat4 = Mat4.identity();
|
||||
readonly projectionView: Mat4 = Mat4.identity();
|
||||
readonly inverseProjectionView: Mat4 = Mat4.identity();
|
||||
|
||||
private pixelScale: number
|
||||
get pixelRatio () {
|
||||
const dpr = (typeof window !== 'undefined') ? window.devicePixelRatio : 1;
|
||||
return dpr * this.pixelScale;
|
||||
}
|
||||
|
||||
readonly viewport: Viewport;
|
||||
readonly state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
readonly viewOffset: Camera.ViewOffset = {
|
||||
enabled: false,
|
||||
fullWidth: 1, fullHeight: 1,
|
||||
offsetX: 0, offsetY: 0,
|
||||
width: 1, height: 1
|
||||
}
|
||||
readonly viewOffset = Camera.ViewOffset();
|
||||
|
||||
near = 1
|
||||
far = 10000
|
||||
@@ -52,6 +67,10 @@ class Camera {
|
||||
|
||||
update() {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
if (snapshot.radiusMax === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const height = 2 * Math.tan(snapshot.fov / 2) * Vec3.distance(snapshot.position, snapshot.target);
|
||||
this.zoom = this.viewport.height / height;
|
||||
|
||||
@@ -86,12 +105,7 @@ class Camera {
|
||||
}
|
||||
|
||||
getTargetDistance(radius: number) {
|
||||
const r = Math.max(radius, 0.01);
|
||||
const { fov } = this.state;
|
||||
const { width, height } = this.viewport;
|
||||
const aspect = width / height;
|
||||
const aspectFactor = (height < width ? 1 : aspect);
|
||||
return Math.abs((r / aspectFactor) / Math.sin(fov / 2));
|
||||
return Camera.targetDistance(radius, this.state.fov, this.viewport.width, this.viewport.height);
|
||||
}
|
||||
|
||||
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
|
||||
@@ -126,8 +140,9 @@ class Camera {
|
||||
return cameraUnproject(out, point, this.viewport, this.inverseProjectionView);
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(-1, -1, 1, 1)) {
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128), props: Partial<{ pixelScale: number }> = {}) {
|
||||
this.viewport = viewport;
|
||||
this.pixelScale = props.pixelScale || 1;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
}
|
||||
}
|
||||
@@ -150,6 +165,15 @@ namespace Camera {
|
||||
height: number
|
||||
}
|
||||
|
||||
export function ViewOffset(): ViewOffset {
|
||||
return {
|
||||
enabled: false,
|
||||
fullWidth: 1, fullHeight: 1,
|
||||
offsetX: 0, offsetY: 0,
|
||||
width: 1, height: 1
|
||||
};
|
||||
}
|
||||
|
||||
export function setViewOffset(out: ViewOffset, fullWidth: number, fullHeight: number, offsetX: number, offsetY: number, width: number, height: number) {
|
||||
out.fullWidth = fullWidth;
|
||||
out.fullHeight = fullHeight;
|
||||
@@ -159,6 +183,23 @@ namespace Camera {
|
||||
out.height = height;
|
||||
}
|
||||
|
||||
export function copyViewOffset(out: ViewOffset, view: ViewOffset) {
|
||||
out.enabled = view.enabled;
|
||||
out.fullWidth = view.fullWidth;
|
||||
out.fullHeight = view.fullHeight;
|
||||
out.offsetX = view.offsetX;
|
||||
out.offsetY = view.offsetY;
|
||||
out.width = view.width;
|
||||
out.height = view.height;
|
||||
}
|
||||
|
||||
export function targetDistance(radius: number, fov: number, width: number, height: number) {
|
||||
const r = Math.max(radius, 0.01);
|
||||
const aspect = width / height;
|
||||
const aspectFactor = (height < width ? 1 : aspect);
|
||||
return Math.abs((r / aspectFactor) / Math.sin(fov / 2));
|
||||
}
|
||||
|
||||
export function createDefaultSnapshot(): Snapshot {
|
||||
return {
|
||||
mode: 'perspective',
|
||||
@@ -211,10 +252,10 @@ namespace Camera {
|
||||
function updateOrtho(camera: Camera) {
|
||||
const { viewport, zoom, near, far, viewOffset } = camera;
|
||||
|
||||
const fullLeft = -(viewport.width - viewport.x) / 2;
|
||||
const fullRight = (viewport.width - viewport.x) / 2;
|
||||
const fullTop = (viewport.height - viewport.y) / 2;
|
||||
const fullBottom = -(viewport.height - viewport.y) / 2;
|
||||
const fullLeft = -viewport.width / 2;
|
||||
const fullRight = viewport.width / 2;
|
||||
const fullTop = viewport.height / 2;
|
||||
const fullBottom = -viewport.height / 2;
|
||||
|
||||
const dx = (fullRight - fullLeft) / (2 * zoom);
|
||||
const dy = (fullTop - fullBottom) / (2 * zoom);
|
||||
@@ -283,7 +324,7 @@ function updateClip(camera: Camera) {
|
||||
|
||||
if (mode === 'perspective') {
|
||||
// set at least to 5 to avoid slow sphere impostor rendering
|
||||
near = Math.max(5, near);
|
||||
near = Math.max(Math.min(radiusMax, 5), near);
|
||||
far = Math.max(5, far);
|
||||
} else {
|
||||
near = Math.max(0, near);
|
||||
|
||||
141
src/mol-canvas3d/camera/stereo.ts
Normal file
141
src/mol-canvas3d/camera/stereo.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*
|
||||
* Adapted from three.js, The MIT License, Copyright © 2010-2020 three.js authors
|
||||
*/
|
||||
|
||||
import { Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { Viewport } from './util';
|
||||
|
||||
export const StereoCameraParams = {
|
||||
eyeSeparation: PD.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }),
|
||||
focus: PD.Numeric(10, { min: 1, max: 100, step: 0.01 }),
|
||||
};
|
||||
export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams);
|
||||
export type StereoCameraProps = PD.Values<typeof StereoCameraParams>
|
||||
|
||||
export { StereoCamera };
|
||||
|
||||
class StereoCamera {
|
||||
readonly left: ICamera = new EyeCamera();
|
||||
readonly right: ICamera = new EyeCamera();
|
||||
|
||||
get viewport() {
|
||||
return this.parent.viewport;
|
||||
}
|
||||
|
||||
get viewOffset() {
|
||||
return this.parent.viewOffset;
|
||||
}
|
||||
|
||||
private props: StereoCameraProps
|
||||
|
||||
constructor(private parent: Camera, props: Partial<StereoCameraProps> = {}) {
|
||||
this.props = { ...DefaultStereoCameraProps, ...props };
|
||||
}
|
||||
|
||||
setProps(props: Partial<StereoCameraProps>) {
|
||||
Object.assign(this.props, props);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.parent.update();
|
||||
update(this.parent, this.props, this.left as EyeCamera, this.right as EyeCamera);
|
||||
}
|
||||
}
|
||||
|
||||
namespace StereoCamera {
|
||||
export function is(camera: Camera | StereoCamera): camera is StereoCamera {
|
||||
return 'left' in camera && 'right' in camera;
|
||||
}
|
||||
}
|
||||
|
||||
class EyeCamera implements ICamera {
|
||||
viewport = Viewport.create(0, 0, 0, 0);
|
||||
view = Mat4();
|
||||
projection = Mat4();
|
||||
projectionView = Mat4();
|
||||
inverseProjectionView = Mat4();
|
||||
state: Readonly<Camera.Snapshot> = Camera.createDefaultSnapshot();
|
||||
viewOffset: Readonly<Camera.ViewOffset> = Camera.ViewOffset();
|
||||
far: number = 0;
|
||||
near: number = 0;
|
||||
fogFar: number = 0;
|
||||
fogNear: number = 0;
|
||||
}
|
||||
|
||||
const eyeLeft = Mat4.identity(), eyeRight = Mat4.identity();
|
||||
|
||||
function update(camera: Camera, props: StereoCameraProps, left: EyeCamera, right: EyeCamera) {
|
||||
// Copy the states
|
||||
|
||||
Viewport.copy(left.viewport, camera.viewport);
|
||||
Mat4.copy(left.view, camera.view);
|
||||
Mat4.copy(left.projection, camera.projection);
|
||||
Camera.copySnapshot(left.state, camera.state);
|
||||
Camera.copyViewOffset(left.viewOffset, camera.viewOffset);
|
||||
left.far = camera.far;
|
||||
left.near = camera.near;
|
||||
left.fogFar = camera.fogFar;
|
||||
left.fogNear = camera.fogNear;
|
||||
|
||||
Viewport.copy(right.viewport, camera.viewport);
|
||||
Mat4.copy(right.view, camera.view);
|
||||
Mat4.copy(right.projection, camera.projection);
|
||||
Camera.copySnapshot(right.state, camera.state);
|
||||
Camera.copyViewOffset(right.viewOffset, camera.viewOffset);
|
||||
right.far = camera.far;
|
||||
right.near = camera.near;
|
||||
right.fogFar = camera.fogFar;
|
||||
right.fogNear = camera.fogNear;
|
||||
|
||||
// update the view offsets
|
||||
|
||||
const w = Math.floor(camera.viewport.width / 2);
|
||||
const aspect = w / camera.viewport.height;
|
||||
|
||||
left.viewport.width = w;
|
||||
right.viewport.x += w;
|
||||
right.viewport.width -= w;
|
||||
|
||||
// update the projection and view matrices
|
||||
|
||||
const eyeSepHalf = props.eyeSeparation / 2;
|
||||
const eyeSepOnProjection = eyeSepHalf * camera.near / props.focus;
|
||||
const ymax = camera.near * Math.tan(camera.state.fov * 0.5);
|
||||
let xmin, xmax;
|
||||
|
||||
// translate xOffset
|
||||
|
||||
eyeLeft[12] = -eyeSepHalf;
|
||||
eyeRight[12] = eyeSepHalf;
|
||||
|
||||
// for left eye
|
||||
|
||||
xmin = -ymax * aspect + eyeSepOnProjection;
|
||||
xmax = ymax * aspect + eyeSepOnProjection;
|
||||
|
||||
left.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
left.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(left.view, left.view, eyeLeft);
|
||||
Mat4.mul(left.projectionView, left.projection, left.view);
|
||||
Mat4.invert(left.inverseProjectionView, left.projectionView);
|
||||
|
||||
// for right eye
|
||||
|
||||
xmin = -ymax * aspect - eyeSepOnProjection;
|
||||
xmax = ymax * aspect - eyeSepOnProjection;
|
||||
|
||||
right.projection[0] = 2 * camera.near / (xmax - xmin);
|
||||
right.projection[8] = (xmax + xmin) / (xmax - xmin);
|
||||
|
||||
Mat4.mul(right.view, right.view, eyeRight);
|
||||
Mat4.mul(right.projectionView, right.projection, right.view);
|
||||
Mat4.invert(right.inverseProjectionView, right.projectionView);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { now } from '../mol-util/now';
|
||||
import { Vec3 } from '../mol-math/linear-algebra';
|
||||
import { Vec3, Vec2 } from '../mol-math/linear-algebra';
|
||||
import InputObserver, { ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
|
||||
import Renderer, { RendererStats, RendererParams } from '../mol-gl/renderer';
|
||||
import { GraphicsRenderObject } from '../mol-gl/render-object';
|
||||
@@ -16,32 +16,38 @@ import { Viewport } from './camera/util';
|
||||
import { createContext, WebGLContext, getGLContext } from '../mol-gl/webgl/context';
|
||||
import { Representation } from '../mol-repr/representation';
|
||||
import Scene from '../mol-gl/scene';
|
||||
import { GraphicsRenderVariant } from '../mol-gl/webgl/render-item';
|
||||
import { PickingId } from '../mol-geo/geometry/picking';
|
||||
import { MarkerAction } from '../mol-util/marker-action';
|
||||
import { Loci, EmptyLoci, isEmptyLoci } from '../mol-model/loci';
|
||||
import { Camera } from './camera';
|
||||
import { ParamDefinition as PD } from '../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { SetUtils } from '../mol-util/set';
|
||||
import { Canvas3dInteractionHelper } from './helper/interaction-events';
|
||||
import { PostprocessingParams, PostprocessingPass } from './passes/postprocessing';
|
||||
import { MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
|
||||
import { PixelData } from '../mol-util/image';
|
||||
import { readTexture } from '../mol-gl/compute/util';
|
||||
import { DrawPass } from './passes/draw';
|
||||
import { PickPass } from './passes/pick';
|
||||
import { MultiSampleHelper, MultiSampleParams, MultiSamplePass } from './passes/multi-sample';
|
||||
import { PickData } from './passes/pick';
|
||||
import { PickHelper } from './passes/pick';
|
||||
import { ImagePass, ImageProps } from './passes/image';
|
||||
import { Sphere3D } from '../mol-math/geometry';
|
||||
import { isDebugMode } from '../mol-util/debug';
|
||||
import { CameraHelperParams } from './helper/camera-helper';
|
||||
import { produce } from 'immer';
|
||||
import { HandleHelper, HandleHelperParams } from './helper/handle-helper';
|
||||
import { HandleHelperParams } from './helper/handle-helper';
|
||||
import { StereoCamera, StereoCameraParams } from './camera/stereo';
|
||||
import { Helper } from './helper/helper';
|
||||
import { Passes } from './passes/passes';
|
||||
import { shallowEqual } from '../mol-util';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
camera: PD.Group({
|
||||
mode: PD.Select('perspective', [['perspective', 'Perspective'], ['orthographic', 'Orthographic']] as const, { label: 'Camera' }),
|
||||
helper: PD.Group(CameraHelperParams, { isFlat: true })
|
||||
mode: PD.Select('perspective', PD.arrayToOptions(['perspective', 'orthographic'] as const), { label: 'Camera' }),
|
||||
helper: PD.Group(CameraHelperParams, { isFlat: true }),
|
||||
stereo: PD.MappedStatic('off', {
|
||||
on: PD.Group(StereoCameraParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
|
||||
manualReset: PD.Boolean(false, { isHidden: true })
|
||||
}, { pivot: 'mode' }),
|
||||
cameraFog: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
@@ -53,6 +59,15 @@ export const Canvas3DParams = {
|
||||
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
|
||||
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
|
||||
}, { pivot: 'radius' }),
|
||||
viewport: PD.MappedStatic('canvas', {
|
||||
canvas: PD.Group({}),
|
||||
custom: PD.Group({
|
||||
x: PD.Numeric(0),
|
||||
y: PD.Numeric(0),
|
||||
width: PD.Numeric(128),
|
||||
height: PD.Numeric(128)
|
||||
})
|
||||
}),
|
||||
|
||||
cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
|
||||
transparentBackground: PD.Boolean(false),
|
||||
@@ -66,6 +81,9 @@ export const Canvas3DParams = {
|
||||
};
|
||||
export const DefaultCanvas3DParams = PD.getDefaultValues(Canvas3DParams);
|
||||
export type Canvas3DProps = PD.Values<typeof Canvas3DParams>
|
||||
export type PartialCanvas3DProps = {
|
||||
[K in keyof Canvas3DProps]?: Canvas3DProps[K] extends { name: string, params: any } ? Canvas3DProps[K] : Partial<Canvas3DProps[K]>
|
||||
}
|
||||
|
||||
export { Canvas3D };
|
||||
|
||||
@@ -78,26 +96,36 @@ interface Canvas3D {
|
||||
* This function must be called if animate() is not set up so that add/remove actions take place.
|
||||
*/
|
||||
commit(isSynchronous?: boolean): void
|
||||
/**
|
||||
* Funcion for external "animation" control
|
||||
* Calls commit.
|
||||
*/
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
|
||||
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
|
||||
requestDraw(force?: boolean): void
|
||||
animate(): void
|
||||
identify(x: number, y: number): PickingId | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId): Representation.Loci
|
||||
|
||||
/** Reset the timers, used by "animate" */
|
||||
resetTime(t: number): void
|
||||
animate(): void
|
||||
pause(): void
|
||||
identify(x: number, y: number): PickData | undefined
|
||||
mark(loci: Representation.Loci, action: MarkerAction): void
|
||||
getLoci(pickingId: PickingId | undefined): Representation.Loci
|
||||
|
||||
notifyDidDraw: boolean,
|
||||
readonly didDraw: BehaviorSubject<now.Timestamp>
|
||||
readonly reprCount: BehaviorSubject<number>
|
||||
readonly resized: BehaviorSubject<any>
|
||||
|
||||
handleResize(): void
|
||||
/** Focuses camera on scene's bounding sphere, centered and zoomed. */
|
||||
requestCameraReset(options?: { durationMs?: number, snapshot?: Partial<Camera.Snapshot> }): void
|
||||
readonly camera: Camera
|
||||
readonly boundingSphere: Readonly<Sphere3D>
|
||||
getPixelData(variant: GraphicsRenderVariant): PixelData
|
||||
setProps(props: Partial<Canvas3DProps> | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void)): void
|
||||
setProps(props: PartialCanvas3DProps | ((old: Canvas3DProps) => Partial<Canvas3DProps> | void), doNotRequestDraw?: boolean /* = false */): void
|
||||
getImagePass(props: Partial<ImageProps>): ImagePass
|
||||
|
||||
/** Returns a copy of the current Canvas3D instance props */
|
||||
@@ -109,23 +137,33 @@ interface Canvas3D {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
const requestAnimationFrame = typeof window !== 'undefined' ? window.requestAnimationFrame : (f: (time: number) => void) => setImmediate(()=>f(Date.now()));
|
||||
const requestAnimationFrame = typeof window !== 'undefined'
|
||||
? window.requestAnimationFrame
|
||||
: (f: (time: number) => void) => setImmediate(() => f(Date.now())) as unknown as number;
|
||||
const cancelAnimationFrame = typeof window !== 'undefined'
|
||||
? window.cancelAnimationFrame
|
||||
: (handle: number) => clearImmediate(handle as unknown as NodeJS.Immediate);
|
||||
|
||||
namespace Canvas3D {
|
||||
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
|
||||
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys }
|
||||
export interface HoverEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
|
||||
export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
|
||||
export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
|
||||
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}) {
|
||||
export function fromCanvas(canvas: HTMLCanvasElement, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ antialias: boolean, pixelScale: number, pickScale: number, enableWboit: boolean }> = {}) {
|
||||
const antialias = (attribs.antialias ?? true) && !attribs.enableWboit;
|
||||
const gl = getGLContext(canvas, {
|
||||
alpha: true,
|
||||
antialias: true,
|
||||
antialias,
|
||||
depth: true,
|
||||
preserveDrawingBuffer: true,
|
||||
premultipliedAlpha: false,
|
||||
premultipliedAlpha: true,
|
||||
});
|
||||
if (gl === null) throw new Error('Could not create a WebGL rendering context');
|
||||
const input = InputObserver.fromElement(canvas);
|
||||
const webgl = createContext(gl);
|
||||
|
||||
const { pixelScale } = attribs;
|
||||
const input = InputObserver.fromElement(canvas, { pixelScale });
|
||||
const webgl = createContext(gl, { pixelScale });
|
||||
const passes = new Passes(webgl, attribs);
|
||||
|
||||
if (isDebugMode) {
|
||||
const loseContextExt = gl.getExtension('WEBGL_lose_context');
|
||||
@@ -160,23 +198,34 @@ namespace Canvas3D {
|
||||
if (isDebugMode) console.log('context restored');
|
||||
}, false);
|
||||
|
||||
return Canvas3D.create(webgl, input, props);
|
||||
// disable postprocessing anti-aliasing if canvas anti-aliasing is enabled
|
||||
if (antialias && !props.postprocessing?.antialiasing) {
|
||||
props.postprocessing = {
|
||||
...DefaultCanvas3DParams.postprocessing,
|
||||
antialiasing: { name: 'off', params: {} }
|
||||
};
|
||||
}
|
||||
|
||||
return create(webgl, input, passes, props, { pixelScale });
|
||||
}
|
||||
|
||||
export function create(webgl: WebGLContext, input: InputObserver, props: Partial<Canvas3DProps> = {}): Canvas3D {
|
||||
const p = { ...DefaultCanvas3DParams, ...props };
|
||||
export function create(webgl: WebGLContext, input: InputObserver, passes: Passes, props: Partial<Canvas3DProps> = {}, attribs: Partial<{ pixelScale: number }>): Canvas3D {
|
||||
const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
|
||||
|
||||
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
|
||||
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
|
||||
const reprCount = new BehaviorSubject(0);
|
||||
|
||||
const startTime = now();
|
||||
let startTime = now();
|
||||
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
|
||||
|
||||
const { gl, contextRestored } = webgl;
|
||||
|
||||
let width = gl.drawingBufferWidth;
|
||||
let height = gl.drawingBufferHeight;
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let width = 128;
|
||||
let height = 128;
|
||||
updateViewport();
|
||||
|
||||
const scene = Scene.create(webgl);
|
||||
|
||||
@@ -185,40 +234,40 @@ namespace Canvas3D {
|
||||
mode: p.camera.mode,
|
||||
fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
|
||||
clipFar: p.cameraClipping.far
|
||||
});
|
||||
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
|
||||
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
|
||||
|
||||
const controls = TrackballControls.create(input, camera, p.trackball);
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
const handleHelper = new HandleHelper(webgl, p.handle);
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
|
||||
const helper = new Helper(webgl, scene, p);
|
||||
|
||||
const drawPass = new DrawPass(webgl, renderer, scene, camera, debugHelper, handleHelper, {
|
||||
cameraHelper: p.camera.helper
|
||||
});
|
||||
const pickPass = new PickPass(webgl, renderer, scene, camera, handleHelper, 0.5);
|
||||
const postprocessing = new PostprocessingPass(webgl, camera, drawPass, p.postprocessing);
|
||||
const multiSample = new MultiSamplePass(webgl, camera, drawPass, postprocessing, p.multiSample);
|
||||
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height });
|
||||
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera);
|
||||
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
|
||||
|
||||
let drawPending = false;
|
||||
let cameraResetRequested = false;
|
||||
let nextCameraResetDuration: number | undefined = void 0;
|
||||
let nextCameraResetSnapshot: Partial<Camera.Snapshot> | undefined = void 0;
|
||||
|
||||
function getLoci(pickingId: PickingId) {
|
||||
let notifyDidDraw = true;
|
||||
|
||||
function getLoci(pickingId: PickingId | undefined) {
|
||||
let loci: Loci = EmptyLoci;
|
||||
let repr: Representation.Any = Representation.Empty;
|
||||
loci = handleHelper.getLoci(pickingId);
|
||||
reprRenderObjects.forEach((_, _repr) => {
|
||||
const _loci = _repr.getLoci(pickingId);
|
||||
if (!isEmptyLoci(_loci)) {
|
||||
if (!isEmptyLoci(loci)) {
|
||||
console.warn('found another loci, this should not happen');
|
||||
if (pickingId) {
|
||||
loci = helper.handle.getLoci(pickingId);
|
||||
reprRenderObjects.forEach((_, _repr) => {
|
||||
const _loci = _repr.getLoci(pickingId);
|
||||
if (!isEmptyLoci(_loci)) {
|
||||
if (!isEmptyLoci(loci)) {
|
||||
console.warn('found another loci, this should not happen');
|
||||
}
|
||||
loci = _loci;
|
||||
repr = _repr;
|
||||
}
|
||||
loci = _loci;
|
||||
repr = _repr;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return { loci, repr };
|
||||
}
|
||||
|
||||
@@ -228,36 +277,44 @@ namespace Canvas3D {
|
||||
if (repr) {
|
||||
changed = repr.mark(loci, action);
|
||||
} else {
|
||||
changed = handleHelper.mark(loci, action);
|
||||
changed = helper.handle.mark(loci, action);
|
||||
reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed; });
|
||||
}
|
||||
if (changed) {
|
||||
scene.update(void 0, true);
|
||||
handleHelper.scene.update(void 0, true);
|
||||
const prevPickDirty = pickPass.pickDirty;
|
||||
helper.handle.scene.update(void 0, true);
|
||||
const prevPickDirty = pickHelper.dirty;
|
||||
draw(true);
|
||||
pickPass.pickDirty = prevPickDirty; // marking does not change picking buffers
|
||||
pickHelper.dirty = prevPickDirty; // marking does not change picking buffers
|
||||
}
|
||||
}
|
||||
|
||||
function render(force: boolean) {
|
||||
if (webgl.isContextLost) return false;
|
||||
if (x > gl.drawingBufferWidth || x + width < 0 ||
|
||||
y > gl.drawingBufferHeight || y + height < 0
|
||||
) return false;
|
||||
|
||||
let didRender = false;
|
||||
controls.update(currentTime);
|
||||
Viewport.set(camera.viewport, 0, 0, width, height);
|
||||
const cameraChanged = camera.update();
|
||||
multiSample.update(force || cameraChanged, currentTime);
|
||||
const multiSampleChanged = multiSampleHelper.update(force || cameraChanged, p.multiSample);
|
||||
|
||||
if (force || cameraChanged || multiSample.enabled) {
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
if (multiSample.enabled) {
|
||||
multiSample.render(true, p.transparentBackground);
|
||||
} else {
|
||||
drawPass.render(!postprocessing.enabled, p.transparentBackground);
|
||||
if (postprocessing.enabled) postprocessing.render(true);
|
||||
if (force || cameraChanged || multiSampleChanged) {
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
cam = stereoCamera;
|
||||
}
|
||||
pickPass.pickDirty = true;
|
||||
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
multiSampleHelper.render(renderer, cam, scene, helper, true, p.transparentBackground, p);
|
||||
} else {
|
||||
const toDrawingBuffer = !PostprocessingPass.isEnabled(p.postprocessing) && scene.volumes.renderables.length === 0 && !passes.draw.wboitEnabled;
|
||||
passes.draw.render(renderer, cam, scene, helper, toDrawingBuffer, p.transparentBackground);
|
||||
if (!toDrawingBuffer) passes.postprocessing.render(cam, true, p.postprocessing);
|
||||
}
|
||||
pickHelper.dirty = true;
|
||||
didRender = true;
|
||||
}
|
||||
|
||||
@@ -269,7 +326,7 @@ namespace Canvas3D {
|
||||
let currentTime = 0;
|
||||
|
||||
function draw(force?: boolean) {
|
||||
if (render(!!force || forceNextDraw)) {
|
||||
if (render(!!force || forceNextDraw) && notifyDidDraw) {
|
||||
didDraw.next(now() - startTime as now.Timestamp);
|
||||
}
|
||||
forceNextDraw = false;
|
||||
@@ -282,20 +339,46 @@ namespace Canvas3D {
|
||||
forceNextDraw = !!force;
|
||||
}
|
||||
|
||||
function animate() {
|
||||
currentTime = now();
|
||||
commit();
|
||||
let animationFrameHandle = 0;
|
||||
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
|
||||
currentTime = t;
|
||||
commit(options?.isSynchronous);
|
||||
camera.transition.tick(currentTime);
|
||||
|
||||
if (options?.manualDraw) {
|
||||
return;
|
||||
}
|
||||
|
||||
draw(false);
|
||||
if (!camera.transition.inTransition && !webgl.isContextLost) {
|
||||
interactionHelper.tick(currentTime);
|
||||
}
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
|
||||
function identify(x: number, y: number): PickingId | undefined {
|
||||
return webgl.isContextLost ? undefined : pickPass.identify(x, y);
|
||||
function _animate() {
|
||||
tick(now());
|
||||
animationFrameHandle = requestAnimationFrame(_animate);
|
||||
}
|
||||
|
||||
function resetTime(t: now.Timestamp) {
|
||||
startTime = t;
|
||||
controls.start(t);
|
||||
}
|
||||
|
||||
function animate() {
|
||||
controls.start(now());
|
||||
if (animationFrameHandle === 0) _animate();
|
||||
}
|
||||
|
||||
function pause() {
|
||||
cancelAnimationFrame(animationFrameHandle);
|
||||
animationFrameHandle = 0;
|
||||
}
|
||||
|
||||
function identify(x: number, y: number): PickData | undefined {
|
||||
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
|
||||
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
|
||||
}
|
||||
|
||||
function commit(isSynchronous: boolean = false) {
|
||||
@@ -304,7 +387,7 @@ namespace Canvas3D {
|
||||
if (allCommited) {
|
||||
resolveCameraReset();
|
||||
if (forceDrawAfterAllCommited) {
|
||||
if (debugHelper.isEnabled) debugHelper.update();
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
draw(true);
|
||||
forceDrawAfterAllCommited = false;
|
||||
}
|
||||
@@ -319,7 +402,7 @@ namespace Canvas3D {
|
||||
const duration = nextCameraResetDuration === undefined ? p.cameraResetDurationMs : nextCameraResetDuration;
|
||||
const focus = camera.getFocus(center, radius);
|
||||
const snapshot = nextCameraResetSnapshot ? { ...focus, ...nextCameraResetSnapshot } : focus;
|
||||
camera.setState(snapshot, duration);
|
||||
camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
|
||||
}
|
||||
|
||||
nextCameraResetDuration = void 0;
|
||||
@@ -346,7 +429,8 @@ namespace Canvas3D {
|
||||
const b = r.values.boundingSphere.ref.value;
|
||||
if (!b.radius) continue;
|
||||
|
||||
if (!Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
|
||||
const cameraDist = Vec3.distance(cameraSphere.center, b.center);
|
||||
if ((cameraDist > cameraSphere.radius || cameraDist > b.radius || b.radius > camera.state.radiusMax) && !Sphere3D.includes(oldBoundingSphereVisible, b)) return true;
|
||||
if (Sphere3D.overlaps(cameraSphere, b)) cameraSphereOverlapsNone = false;
|
||||
}
|
||||
|
||||
@@ -362,8 +446,8 @@ namespace Canvas3D {
|
||||
|
||||
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
|
||||
|
||||
if (debugHelper.isEnabled) debugHelper.update();
|
||||
if (reprCount.value === 0 || shouldResetCamera()) {
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
|
||||
cameraResetRequested = true;
|
||||
}
|
||||
if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
|
||||
@@ -381,6 +465,7 @@ namespace Canvas3D {
|
||||
instanceCount: r.values.instanceCount.ref.value,
|
||||
materialId: r.materialId,
|
||||
})));
|
||||
console.log(webgl.stats);
|
||||
}
|
||||
|
||||
function add(repr: Representation.Any) {
|
||||
@@ -412,7 +497,6 @@ namespace Canvas3D {
|
||||
if (renderObjects) {
|
||||
renderObjects.forEach(o => scene.remove(o));
|
||||
reprRenderObjects.delete(repr);
|
||||
scene.update(repr.renderObjects, false, true);
|
||||
forceDrawAfterAllCommited = true;
|
||||
if (isDebugMode) consoleStats();
|
||||
}
|
||||
@@ -442,7 +526,9 @@ namespace Canvas3D {
|
||||
return {
|
||||
camera: {
|
||||
mode: camera.state.mode,
|
||||
helper: { ...drawPass.props.cameraHelper }
|
||||
helper: { ...helper.camera.props },
|
||||
stereo: { ...p.camera.stereo },
|
||||
manualReset: !!p.camera.manualReset
|
||||
},
|
||||
cameraFog: camera.state.fog > 0
|
||||
? { name: 'on' as const, params: { intensity: camera.state.fog } }
|
||||
@@ -450,23 +536,24 @@ namespace Canvas3D {
|
||||
cameraClipping: { far: camera.state.clipFar, radius },
|
||||
cameraResetDurationMs: p.cameraResetDurationMs,
|
||||
transparentBackground: p.transparentBackground,
|
||||
viewport: p.viewport,
|
||||
|
||||
postprocessing: { ...postprocessing.props },
|
||||
multiSample: { ...multiSample.props },
|
||||
postprocessing: { ...p.postprocessing },
|
||||
multiSample: { ...p.multiSample },
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
debug: { ...debugHelper.props },
|
||||
handle: { ...handleHelper.props },
|
||||
debug: { ...helper.debug.props },
|
||||
handle: { ...helper.handle.props },
|
||||
};
|
||||
}
|
||||
|
||||
handleResize();
|
||||
|
||||
const contextRestoredSub = contextRestored.subscribe(() => {
|
||||
pickPass.pickDirty = true;
|
||||
pickHelper.dirty = true;
|
||||
draw(true);
|
||||
});
|
||||
|
||||
const resized = new BehaviorSubject<any>(0);
|
||||
|
||||
return {
|
||||
webgl,
|
||||
|
||||
@@ -487,7 +574,7 @@ namespace Canvas3D {
|
||||
reprUpdatedSubscriptions.clear();
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
debugHelper.clear();
|
||||
helper.debug.clear();
|
||||
requestDraw(true);
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
@@ -498,19 +585,27 @@ namespace Canvas3D {
|
||||
}
|
||||
|
||||
if (scene.syncVisibility()) {
|
||||
if (debugHelper.isEnabled) debugHelper.update();
|
||||
if (helper.debug.isEnabled) helper.debug.update();
|
||||
}
|
||||
requestDraw(true);
|
||||
},
|
||||
|
||||
// draw,
|
||||
requestDraw,
|
||||
tick,
|
||||
animate,
|
||||
resetTime,
|
||||
pause,
|
||||
identify,
|
||||
mark,
|
||||
getLoci,
|
||||
|
||||
handleResize,
|
||||
handleResize: () => {
|
||||
passes.updateSize();
|
||||
updateViewport();
|
||||
syncViewport();
|
||||
requestDraw(true);
|
||||
resized.next(+new Date());
|
||||
},
|
||||
requestCameraReset: options => {
|
||||
nextCameraResetDuration = options?.durationMs;
|
||||
nextCameraResetSnapshot = options?.snapshot;
|
||||
@@ -518,19 +613,13 @@ namespace Canvas3D {
|
||||
},
|
||||
camera,
|
||||
boundingSphere: scene.boundingSphere,
|
||||
getPixelData: (variant: GraphicsRenderVariant) => {
|
||||
switch (variant) {
|
||||
case 'color': return webgl.getDrawingBufferPixelData();
|
||||
case 'pickObject': return pickPass.objectPickTarget.getPixelData();
|
||||
case 'pickInstance': return pickPass.instancePickTarget.getPixelData();
|
||||
case 'pickGroup': return pickPass.groupPickTarget.getPixelData();
|
||||
case 'depth': return readTexture(webgl, drawPass.depthTexture) as PixelData;
|
||||
}
|
||||
},
|
||||
get notifyDidDraw() { return notifyDidDraw; },
|
||||
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
|
||||
didDraw,
|
||||
reprCount,
|
||||
setProps: (properties) => {
|
||||
const props: Partial<Canvas3DProps> = typeof properties === 'function'
|
||||
resized,
|
||||
setProps: (properties, doNotRequestDraw = false) => {
|
||||
const props: PartialCanvas3DProps = typeof properties === 'function'
|
||||
? produce(getProps(), properties)
|
||||
: properties;
|
||||
|
||||
@@ -538,7 +627,7 @@ namespace Canvas3D {
|
||||
if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
|
||||
cameraState.mode = props.camera.mode;
|
||||
}
|
||||
if (props.cameraFog !== undefined) {
|
||||
if (props.cameraFog !== undefined && props.cameraFog.params) {
|
||||
const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
|
||||
if (newFog !== camera.state.fog) cameraState.fog = newFog;
|
||||
}
|
||||
@@ -556,47 +645,43 @@ namespace Canvas3D {
|
||||
}
|
||||
if (Object.keys(cameraState).length > 0) camera.setState(cameraState);
|
||||
|
||||
if (props.camera?.helper) drawPass.setProps({ cameraHelper: props.camera.helper });
|
||||
if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
|
||||
if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
|
||||
if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
|
||||
if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.viewport !== undefined) {
|
||||
const doNotUpdate = p.viewport === props.viewport ||
|
||||
(p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
|
||||
|
||||
if (props.postprocessing) postprocessing.setProps(props.postprocessing);
|
||||
if (props.multiSample) multiSample.setProps(props.multiSample);
|
||||
if (!doNotUpdate) {
|
||||
p.viewport = props.viewport;
|
||||
updateViewport();
|
||||
syncViewport();
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
|
||||
if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
|
||||
if (props.renderer) renderer.setProps(props.renderer);
|
||||
if (props.trackball) controls.setProps(props.trackball);
|
||||
if (props.debug) debugHelper.setProps(props.debug);
|
||||
if (props.handle) handleHelper.setProps(props.handle);
|
||||
if (props.debug) helper.debug.setProps(props.debug);
|
||||
if (props.handle) helper.handle.setProps(props.handle);
|
||||
|
||||
requestDraw(true);
|
||||
if (cameraState.mode === 'orthographic') {
|
||||
p.camera.stereo.name = 'off';
|
||||
}
|
||||
|
||||
if (!doNotRequestDraw) {
|
||||
requestDraw(true);
|
||||
}
|
||||
},
|
||||
getImagePass: (props: Partial<ImageProps> = {}) => {
|
||||
return new ImagePass(webgl, renderer, scene, camera, debugHelper, handleHelper, props);
|
||||
return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
|
||||
},
|
||||
|
||||
get props() {
|
||||
const radius = scene.boundingSphere.radius > 0
|
||||
? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
camera: {
|
||||
mode: camera.state.mode,
|
||||
helper: { ...drawPass.props.cameraHelper }
|
||||
},
|
||||
cameraFog: camera.state.fog > 0
|
||||
? { name: 'on' as const, params: { intensity: camera.state.fog } }
|
||||
: { name: 'off' as const, params: {} },
|
||||
cameraClipping: { far: camera.state.clipFar, radius },
|
||||
cameraResetDurationMs: p.cameraResetDurationMs,
|
||||
transparentBackground: p.transparentBackground,
|
||||
|
||||
postprocessing: { ...postprocessing.props },
|
||||
multiSample: { ...multiSample.props },
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
debug: { ...debugHelper.props },
|
||||
handle: { ...handleHelper.props },
|
||||
};
|
||||
return getProps();
|
||||
},
|
||||
get input() {
|
||||
return input;
|
||||
@@ -611,7 +696,7 @@ namespace Canvas3D {
|
||||
contextRestoredSub.unsubscribe();
|
||||
|
||||
scene.clear();
|
||||
debugHelper.clear();
|
||||
helper.debug.clear();
|
||||
input.dispose();
|
||||
controls.dispose();
|
||||
renderer.dispose();
|
||||
@@ -619,20 +704,25 @@ namespace Canvas3D {
|
||||
}
|
||||
};
|
||||
|
||||
function handleResize() {
|
||||
width = gl.drawingBufferWidth;
|
||||
height = gl.drawingBufferHeight;
|
||||
function updateViewport() {
|
||||
if (p.viewport.name === 'canvas') {
|
||||
x = 0;
|
||||
y = 0;
|
||||
width = gl.drawingBufferWidth;
|
||||
height = gl.drawingBufferHeight;
|
||||
} else {
|
||||
x = p.viewport.params.x * webgl.pixelRatio;
|
||||
y = p.viewport.params.y * webgl.pixelRatio;
|
||||
width = p.viewport.params.width * webgl.pixelRatio;
|
||||
height = p.viewport.params.height * webgl.pixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
renderer.setViewport(0, 0, width, height);
|
||||
Viewport.set(camera.viewport, 0, 0, width, height);
|
||||
Viewport.set(controls.viewport, 0, 0, width, height);
|
||||
|
||||
drawPass.setSize(width, height);
|
||||
pickPass.setSize(width, height);
|
||||
postprocessing.setSize(width, height);
|
||||
multiSample.setSize(width, height);
|
||||
|
||||
requestDraw(true);
|
||||
function syncViewport() {
|
||||
pickHelper.setViewport(x, y, width, height);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
Viewport.set(camera.viewport, x, y, width, height);
|
||||
Viewport.set(controls.viewport, x, y, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
58
src/mol-canvas3d/controls/object.ts
Normal file
58
src/mol-canvas3d/controls/object.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
|
||||
export namespace ObjectControls {
|
||||
function mouseOnScreen(out: Vec2, page: Vec2, viewport: Viewport) {
|
||||
return Vec2.set(
|
||||
out,
|
||||
(page[0] - viewport.x) / viewport.width,
|
||||
(page[1] - viewport.y) / viewport.height
|
||||
);
|
||||
}
|
||||
|
||||
const panMouseChange = Vec2();
|
||||
const panObjUp = Vec3();
|
||||
const panOffset = Vec3();
|
||||
const eye = Vec3();
|
||||
const panStart = Vec2();
|
||||
const panEnd = Vec2();
|
||||
|
||||
const target = Vec3();
|
||||
|
||||
/**
|
||||
* Get vector for movement in camera projection plane:
|
||||
* `pageStart` and `pageEnd` are 2d window coordinates;
|
||||
* `ref` defines the plane depth, if not given `camera.target` is used
|
||||
*/
|
||||
export function panDirection(out: Vec3, pageStart: Vec2, pageEnd: Vec2, camera: Camera, ref?: Vec3) {
|
||||
mouseOnScreen(panStart, pageStart, camera.viewport);
|
||||
mouseOnScreen(panEnd, pageEnd, camera.viewport);
|
||||
Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, panEnd), panStart);
|
||||
Vec3.sub(eye, camera.position, camera.target);
|
||||
|
||||
if (!ref || camera.state.mode === 'orthographic') Vec3.copy(target, camera.target);
|
||||
else Vec3.projectPointOnVector(target, ref, eye, camera.position);
|
||||
|
||||
const dist = Vec3.distance(camera.position, target);
|
||||
const height = 2 * Math.tan(camera.state.fov / 2) * dist;
|
||||
const zoom = camera.viewport.height / height;
|
||||
|
||||
panMouseChange[0] *= (1 / zoom) * camera.viewport.width * camera.pixelRatio;
|
||||
panMouseChange[1] *= (1 / zoom) * camera.viewport.height * camera.pixelRatio;
|
||||
|
||||
Vec3.cross(panOffset, Vec3.copy(panOffset, eye), camera.up);
|
||||
Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
|
||||
|
||||
Vec3.setMagnitude(panObjUp, camera.up, panMouseChange[1]);
|
||||
Vec3.add(panOffset, panOffset, panObjUp);
|
||||
|
||||
return Vec3.negate(out, panOffset);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export const TrackballControlsParams = {
|
||||
|
||||
rotateSpeed: PD.Numeric(3.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
zoomSpeed: PD.Numeric(6.0, { min: 0.1, max: 10, step: 0.1 }),
|
||||
panSpeed: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
|
||||
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
|
||||
|
||||
spin: PD.Boolean(false, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
spinSpeed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
|
||||
@@ -60,6 +60,7 @@ interface TrackballControls {
|
||||
readonly props: Readonly<TrackballControlsProps>
|
||||
setProps: (props: Partial<TrackballControlsProps>) => void
|
||||
|
||||
start: (t: number) => void
|
||||
update: (t: number) => void
|
||||
reset: () => void
|
||||
dispose: () => void
|
||||
@@ -68,7 +69,7 @@ namespace TrackballControls {
|
||||
export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
|
||||
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
|
||||
|
||||
const viewport = Viewport();
|
||||
const viewport = Viewport.clone(camera.viewport);
|
||||
|
||||
let disposed = false;
|
||||
|
||||
@@ -137,7 +138,7 @@ namespace TrackballControls {
|
||||
const dy = _rotCurr[1] - _rotPrev[1];
|
||||
Vec3.set(rotMoveDir, dx, dy, 0);
|
||||
|
||||
const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed;
|
||||
const angle = Vec3.magnitude(rotMoveDir) * p.rotateSpeed * input.pixelRatio;
|
||||
|
||||
if (angle) {
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
@@ -227,7 +228,9 @@ namespace TrackballControls {
|
||||
Vec2.sub(panMouseChange, Vec2.copy(panMouseChange, _panEnd), _panStart);
|
||||
|
||||
if (Vec2.squaredMagnitude(panMouseChange)) {
|
||||
Vec2.scale(panMouseChange, panMouseChange, Vec3.magnitude(_eye) * p.panSpeed);
|
||||
const factor = input.pixelRatio * p.panSpeed;
|
||||
panMouseChange[0] *= (1 / camera.zoom) * camera.viewport.width * factor;
|
||||
panMouseChange[1] *= (1 / camera.zoom) * camera.viewport.height * factor;
|
||||
|
||||
Vec3.cross(panOffset, Vec3.copy(panOffset, _eye), camera.up);
|
||||
Vec3.setMagnitude(panOffset, panOffset, panMouseChange[0]);
|
||||
@@ -269,11 +272,22 @@ namespace TrackballControls {
|
||||
}
|
||||
}
|
||||
|
||||
function outsideViewport(x: number, y: number) {
|
||||
x *= input.pixelRatio;
|
||||
y *= input.pixelRatio;
|
||||
return (
|
||||
x > viewport.x + viewport.width ||
|
||||
input.height - y > viewport.y + viewport.height ||
|
||||
x < viewport.x ||
|
||||
input.height - y < viewport.y
|
||||
);
|
||||
}
|
||||
|
||||
let lastUpdated = -1;
|
||||
/** Update the object's position, direction and up vectors */
|
||||
function update(t: number) {
|
||||
if (lastUpdated === t) return;
|
||||
if (p.spin) spin(t - lastUpdated);
|
||||
if (p.spin && lastUpdated > 0) spin(t - lastUpdated);
|
||||
|
||||
Vec3.sub(_eye, camera.position, camera.target);
|
||||
|
||||
@@ -305,7 +319,12 @@ namespace TrackballControls {
|
||||
|
||||
// listeners
|
||||
|
||||
function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
|
||||
function onDrag({ x, y, pageX, pageY, buttons, modifiers, isStart }: DragInput) {
|
||||
const isOutside = outsideViewport(x, y);
|
||||
|
||||
if (isStart && isOutside) return;
|
||||
if (!isStart && !_isInteracting) return;
|
||||
|
||||
_isInteracting = true;
|
||||
|
||||
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
|
||||
@@ -356,7 +375,9 @@ namespace TrackballControls {
|
||||
_isInteracting = false;
|
||||
}
|
||||
|
||||
function onWheel({ dx, dy, dz, buttons, modifiers }: WheelInput) {
|
||||
function onWheel({ x, y, dx, dy, dz, buttons, modifiers }: WheelInput) {
|
||||
if (outsideViewport(x, y)) return;
|
||||
|
||||
const delta = absMax(dx, dy, dz);
|
||||
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
|
||||
_zoomEnd[1] += delta * 0.0001;
|
||||
@@ -385,13 +406,17 @@ namespace TrackballControls {
|
||||
|
||||
const _spinSpeed = Vec2.create(0.005, 0);
|
||||
function spin(deltaT: number) {
|
||||
if (p.spinSpeed === 0) return;
|
||||
|
||||
const frameSpeed = (p.spinSpeed || 0) / 1000;
|
||||
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
|
||||
if (!_isInteracting) Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
}
|
||||
|
||||
// force an update at start
|
||||
update(0);
|
||||
function start(t: number) {
|
||||
lastUpdated = -1;
|
||||
update(t);
|
||||
}
|
||||
|
||||
return {
|
||||
viewport,
|
||||
@@ -401,6 +426,7 @@ namespace TrackballControls {
|
||||
Object.assign(p, props);
|
||||
},
|
||||
|
||||
start,
|
||||
update,
|
||||
reset,
|
||||
dispose
|
||||
|
||||
@@ -67,6 +67,7 @@ export class BoundingSphereHelper {
|
||||
uInstanceCount: ro.values.uInstanceCount,
|
||||
instanceCount: ro.values.instanceCount,
|
||||
aInstance: ro.values.aInstance,
|
||||
hasReflection: ro.values.hasReflection,
|
||||
});
|
||||
if (newInstanceData) this.instancesData.set(ro, newInstanceData);
|
||||
});
|
||||
@@ -131,7 +132,7 @@ function updateBoundingSphereData(scene: Scene, boundingSphere: Sphere3D, data:
|
||||
const mesh = createBoundingSphereMesh(boundingSphere, data && data.mesh);
|
||||
const renderObject = data ? data.renderObject : createBoundingSphereRenderObject(mesh, color, materialId, transform);
|
||||
if (data) {
|
||||
ValueCell.update(renderObject.values.drawCount, Geometry.getDrawCount(mesh));
|
||||
ValueCell.updateIfChanged(renderObject.values.drawCount, Geometry.getDrawCount(mesh));
|
||||
} else {
|
||||
scene.add(renderObject);
|
||||
}
|
||||
@@ -159,5 +160,5 @@ const instanceMaterialId = getNextMaterialId();
|
||||
|
||||
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform);
|
||||
return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false, writeDepth: false }, materialId);
|
||||
return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { Camera } from '../camera';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
@@ -15,7 +15,6 @@ import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import produce from 'immer';
|
||||
@@ -87,43 +86,47 @@ export class CameraHelper {
|
||||
return this.props.axes.name === 'on';
|
||||
}
|
||||
|
||||
update(camera: Camera) {
|
||||
update(camera: ICamera) {
|
||||
if (!this.renderObject) return;
|
||||
|
||||
updateCamera(this.camera, camera.viewport);
|
||||
|
||||
const m = this.renderObject.values.aTransform.ref.value as unknown as Mat4;
|
||||
Mat4.extractRotation(m, camera.view);
|
||||
updateCamera(this.camera, camera.viewport, camera.viewOffset);
|
||||
Mat4.extractRotation(this.scene.view, camera.view);
|
||||
|
||||
const r = this.renderObject.values.boundingSphere.ref.value.radius;
|
||||
Mat4.setTranslation(m, Vec3.create(
|
||||
Mat4.setTranslation(this.scene.view, Vec3.create(
|
||||
-camera.viewport.width / 2 + r,
|
||||
-camera.viewport.height / 2 + r,
|
||||
0
|
||||
));
|
||||
|
||||
ValueCell.update(this.renderObject.values.aTransform, this.renderObject.values.aTransform.ref.value);
|
||||
this.scene.update([this.renderObject], true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateCamera(camera: Camera, viewport: Viewport) {
|
||||
function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.ViewOffset) {
|
||||
const { near, far } = camera;
|
||||
|
||||
const fullLeft = -(viewport.width - viewport.x) / 2;
|
||||
const fullRight = (viewport.width - viewport.x) / 2;
|
||||
const fullTop = (viewport.height - viewport.y) / 2;
|
||||
const fullBottom = -(viewport.height - viewport.y) / 2;
|
||||
const fullLeft = -viewport.width / 2;
|
||||
const fullRight = viewport.width / 2;
|
||||
const fullTop = viewport.height / 2;
|
||||
const fullBottom = -viewport.height / 2;
|
||||
|
||||
const dx = (fullRight - fullLeft) / 2;
|
||||
const dy = (fullTop - fullBottom) / 2;
|
||||
const cx = (fullRight + fullLeft) / 2;
|
||||
const cy = (fullTop + fullBottom) / 2;
|
||||
|
||||
const left = cx - dx;
|
||||
const right = cx + dx;
|
||||
const top = cy + dy;
|
||||
const bottom = cy - dy;
|
||||
let left = cx - dx;
|
||||
let right = cx + dx;
|
||||
let top = cy + dy;
|
||||
let bottom = cy - dy;
|
||||
|
||||
if (viewOffset.enabled) {
|
||||
const scaleW = (fullRight - fullLeft) / viewOffset.width;
|
||||
const scaleH = (fullTop - fullBottom) / viewOffset.height;
|
||||
left += scaleW * viewOffset.offsetX;
|
||||
right = left + scaleW * viewOffset.width;
|
||||
top -= scaleH * viewOffset.offsetY;
|
||||
bottom = top - scaleH * viewOffset.height;
|
||||
}
|
||||
|
||||
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
|
||||
}
|
||||
|
||||
37
src/mol-canvas3d/helper/helper.ts
Normal file
37
src/mol-canvas3d/helper/helper.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, DebugHelperParams } from './bounding-sphere-helper';
|
||||
import { CameraHelper, CameraHelperParams } from './camera-helper';
|
||||
import { HandleHelper, HandleHelperParams } from './handle-helper';
|
||||
|
||||
export const HelperParams = {
|
||||
debug: PD.Group(DebugHelperParams),
|
||||
camera: PD.Group({
|
||||
helper: PD.Group(CameraHelperParams)
|
||||
}),
|
||||
handle: PD.Group(HandleHelperParams),
|
||||
};
|
||||
export const DefaultHelperProps = PD.getDefaultValues(HelperParams);
|
||||
export type HelperProps = PD.Values<typeof HelperParams>
|
||||
|
||||
|
||||
export class Helper {
|
||||
readonly debug: BoundingSphereHelper
|
||||
readonly camera: CameraHelper
|
||||
readonly handle: HandleHelper
|
||||
|
||||
constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
|
||||
const p = { ...DefaultHelperProps, ...props };
|
||||
|
||||
this.debug = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
this.camera = new CameraHelper(webgl, p.camera.helper);
|
||||
this.handle = new HandleHelper(webgl, p.handle);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { EmptyLoci } from '../../mol-model/loci';
|
||||
import { Representation } from '../../mol-repr/representation';
|
||||
import InputObserver, { ModifiersKeys, ButtonsType } from '../../mol-util/input/input-observer';
|
||||
import { RxEventHelper } from '../../mol-util/rx-event-helper';
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Camera } from '../camera';
|
||||
|
||||
type Canvas3D = import('../canvas3d').Canvas3D
|
||||
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
|
||||
type DragEvent = import('../canvas3d').Canvas3D.DragEvent
|
||||
type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
|
||||
|
||||
const enum InputEvent { Move, Click, Drag }
|
||||
|
||||
export class Canvas3dInteractionHelper {
|
||||
private ev = RxEventHelper.create();
|
||||
|
||||
readonly events = {
|
||||
hover: this.ev<HoverEvent>(),
|
||||
drag: this.ev<DragEvent>(),
|
||||
click: this.ev<ClickEvent>(),
|
||||
};
|
||||
|
||||
private cX = -1;
|
||||
private cY = -1;
|
||||
|
||||
private lastX = -1;
|
||||
private lastY = -1;
|
||||
private startX = -1;
|
||||
private startY = -1;
|
||||
private endX = -1;
|
||||
private endY = -1;
|
||||
|
||||
private id: PickingId | undefined = void 0;
|
||||
private position: Vec3 | undefined = void 0;
|
||||
|
||||
private currentIdentifyT = 0;
|
||||
private isInteracting = false;
|
||||
|
||||
private prevLoci: Representation.Loci = Representation.Loci.Empty;
|
||||
private prevT = 0;
|
||||
@@ -41,90 +48,138 @@ export class Canvas3dInteractionHelper {
|
||||
private button: ButtonsType.Flag = ButtonsType.create(0);
|
||||
private modifiers: ModifiersKeys = ModifiersKeys.None;
|
||||
|
||||
private identify(isClick: boolean, t: number) {
|
||||
if (this.lastX !== this.cX || this.lastY !== this.cY) {
|
||||
this.id = this.canvasIdentify(this.cX, this.cY);
|
||||
this.lastX = this.cX;
|
||||
this.lastY = this.cY;
|
||||
}
|
||||
private identify(e: InputEvent, t: number) {
|
||||
const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
|
||||
|
||||
if (!this.id) return;
|
||||
if (e === InputEvent.Drag) {
|
||||
if (xyChanged && !Representation.Loci.isEmpty(this.prevLoci)) {
|
||||
this.events.drag.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, pageStart: Vec2.create(this.startX, this.startY), pageEnd: Vec2.create(this.endX, this.endY) });
|
||||
|
||||
if (isClick) {
|
||||
this.events.click.next({ current: this.getLoci(this.id), buttons: this.buttons, button: this.button, modifiers: this.modifiers });
|
||||
this.startX = this.endX;
|
||||
this.startY = this.endY;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.inside || this.currentIdentifyT !== t) {
|
||||
if (xyChanged) {
|
||||
const pickData = this.canvasIdentify(this.endX, this.endY);
|
||||
this.id = pickData?.id;
|
||||
this.position = pickData?.position;
|
||||
this.startX = this.endX;
|
||||
this.startY = this.endY;
|
||||
}
|
||||
|
||||
if (e === InputEvent.Click) {
|
||||
const loci = this.getLoci(this.id);
|
||||
this.events.click.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
this.prevLoci = loci;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.inside || this.currentIdentifyT !== t || !xyChanged || this.outsideViewport(this.endX, this.endY)) return;
|
||||
|
||||
const loci = this.getLoci(this.id);
|
||||
// only broadcast the latest hover
|
||||
if (!Representation.Loci.areEqual(this.prevLoci, loci)) {
|
||||
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers });
|
||||
this.prevLoci = loci;
|
||||
}
|
||||
this.events.hover.next({ current: loci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
this.prevLoci = loci;
|
||||
}
|
||||
|
||||
tick(t: number) {
|
||||
if (this.inside && t - this.prevT > 1000 / this.maxFps) {
|
||||
this.prevT = t;
|
||||
this.currentIdentifyT = t;
|
||||
this.identify(false, t);
|
||||
this.identify(this.isInteracting ? InputEvent.Drag : InputEvent.Move, t);
|
||||
}
|
||||
}
|
||||
|
||||
leave() {
|
||||
private leave() {
|
||||
this.inside = false;
|
||||
if (this.prevLoci.loci !== EmptyLoci) {
|
||||
if (!Representation.Loci.isEmpty(this.prevLoci)) {
|
||||
this.prevLoci = Representation.Loci.Empty;
|
||||
this.events.hover.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers });
|
||||
}
|
||||
}
|
||||
|
||||
move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
private move(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
this.inside = true;
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.cX = x;
|
||||
this.cY = y;
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
}
|
||||
|
||||
select(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
this.cX = x;
|
||||
this.cY = y;
|
||||
private click(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.identify(true, 0);
|
||||
this.identify(InputEvent.Click, 0);
|
||||
}
|
||||
|
||||
modify(modifiers: ModifiersKeys) {
|
||||
if (this.prevLoci.loci === EmptyLoci || ModifiersKeys.areEqual(modifiers, this.modifiers)) return;
|
||||
private drag(x: number, y: number, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys) {
|
||||
this.endX = x;
|
||||
this.endY = y;
|
||||
this.buttons = buttons;
|
||||
this.button = button;
|
||||
this.modifiers = modifiers;
|
||||
this.events.hover.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers });
|
||||
this.identify(InputEvent.Drag, 0);
|
||||
}
|
||||
|
||||
private modify(modifiers: ModifiersKeys) {
|
||||
if (ModifiersKeys.areEqual(modifiers, this.modifiers)) return;
|
||||
this.modifiers = modifiers;
|
||||
this.events.hover.next({ current: this.prevLoci, buttons: this.buttons, button: this.button, modifiers: this.modifiers, page: Vec2.create(this.endX, this.endY), position: this.position });
|
||||
}
|
||||
|
||||
private outsideViewport(x: number, y: number) {
|
||||
const { input, camera: { viewport } } = this;
|
||||
x *= input.pixelRatio;
|
||||
y *= input.pixelRatio;
|
||||
return (
|
||||
x > viewport.x + viewport.width ||
|
||||
input.height - y > viewport.y + viewport.height ||
|
||||
x < viewport.x ||
|
||||
input.height - y < viewport.y
|
||||
);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.ev.dispose();
|
||||
}
|
||||
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], input: InputObserver, private maxFps: number = 30) {
|
||||
constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private maxFps: number = 30) {
|
||||
input.drag.subscribe(({x, y, buttons, button, modifiers }) => {
|
||||
this.isInteracting = true;
|
||||
// console.log('drag');
|
||||
this.drag(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
input.move.subscribe(({x, y, inside, buttons, button, modifiers }) => {
|
||||
if (!inside) return;
|
||||
if (!inside || this.isInteracting) return;
|
||||
// console.log('move');
|
||||
this.move(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
input.leave.subscribe(() => {
|
||||
// console.log('leave');
|
||||
this.leave();
|
||||
});
|
||||
|
||||
input.click.subscribe(({x, y, buttons, button, modifiers }) => {
|
||||
this.select(x, y, buttons, button, modifiers);
|
||||
if (this.outsideViewport(x, y)) return;
|
||||
// console.log('click');
|
||||
this.click(x, y, buttons, button, modifiers);
|
||||
});
|
||||
|
||||
input.modifiers.subscribe(modifiers => this.modify(modifiers));
|
||||
input.interactionEnd.subscribe(() => {
|
||||
// console.log('interactionEnd');
|
||||
this.isInteracting = false;
|
||||
});
|
||||
|
||||
input.modifiers.subscribe(modifiers => {
|
||||
// console.log('modifiers');
|
||||
this.modify(modifiers);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,106 +2,261 @@
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { createNullRenderTarget, RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { Camera } from '../camera';
|
||||
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { HandleHelper } from '../helper/handle-helper';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { Helper } from '../helper/helper';
|
||||
|
||||
export const DrawPassParams = {
|
||||
cameraHelper: PD.Group(CameraHelperParams)
|
||||
import quad_vert from '../../mol-gl/shader/quad.vert';
|
||||
import depthMerge_frag from '../../mol-gl/shader/depth-merge.frag';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
import { WboitPass } from './wboit';
|
||||
|
||||
const DepthMergeSchema = {
|
||||
...QuadSchema,
|
||||
tDepthPrimitives: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
|
||||
tDepthVolumes: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
dPackedDepth: DefineSpec('boolean'),
|
||||
};
|
||||
export const DefaultDrawPassProps = PD.getDefaultValues(DrawPassParams);
|
||||
export type DrawPassProps = PD.Values<typeof DrawPassParams>
|
||||
const DepthMergeShaderCode = ShaderCode('depth-merge', quad_vert, depthMerge_frag);
|
||||
type DepthMergeRenderable = ComputeRenderable<Values<typeof DepthMergeSchema>>
|
||||
|
||||
function getDepthMergeRenderable(ctx: WebGLContext, depthTexturePrimitives: Texture, depthTextureVolumes: Texture, packedDepth: boolean): DepthMergeRenderable {
|
||||
const values: Values<typeof DepthMergeSchema> = {
|
||||
...QuadValues,
|
||||
tDepthPrimitives: ValueCell.create(depthTexturePrimitives),
|
||||
tDepthVolumes: ValueCell.create(depthTextureVolumes),
|
||||
uTexSize: ValueCell.create(Vec2.create(depthTexturePrimitives.getWidth(), depthTexturePrimitives.getHeight())),
|
||||
dPackedDepth: ValueCell.create(packedDepth),
|
||||
};
|
||||
|
||||
const schema = { ...DepthMergeSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', DepthMergeShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
export class DrawPass {
|
||||
colorTarget: RenderTarget
|
||||
depthTexture: Texture
|
||||
packedDepth: boolean
|
||||
private readonly drawTarget: RenderTarget
|
||||
|
||||
cameraHelper: CameraHelper
|
||||
readonly colorTarget: RenderTarget
|
||||
readonly depthTexture: Texture
|
||||
readonly depthTexturePrimitives: Texture
|
||||
|
||||
private depthTarget: RenderTarget | null
|
||||
private readonly packedDepth: boolean
|
||||
private depthTarget: RenderTarget
|
||||
private depthTargetPrimitives: RenderTarget | null
|
||||
private depthTargetVolumes: RenderTarget | null
|
||||
private depthTextureVolumes: Texture
|
||||
private depthMerge: DepthMergeRenderable
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private debugHelper: BoundingSphereHelper, private handleHelper: HandleHelper, props: Partial<DrawPassProps> = {}) {
|
||||
const { gl, extensions, resources } = webgl;
|
||||
const width = gl.drawingBufferWidth;
|
||||
const height = gl.drawingBufferHeight;
|
||||
this.colorTarget = webgl.createRenderTarget(width, height);
|
||||
private wboit: WboitPass | undefined
|
||||
|
||||
get wboitEnabled() {
|
||||
return !!this.wboit?.enabled;
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) {
|
||||
const { extensions, resources } = webgl;
|
||||
|
||||
this.drawTarget = createNullRenderTarget(webgl.gl);
|
||||
|
||||
this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
|
||||
this.packedDepth = !extensions.depthTexture;
|
||||
this.depthTarget = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
|
||||
this.depthTexture = this.depthTarget ? this.depthTarget.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
|
||||
if (!this.packedDepth) {
|
||||
this.depthTexture.define(width, height);
|
||||
this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
}
|
||||
|
||||
const p = { ...DefaultDrawPassProps, ...props };
|
||||
this.cameraHelper = new CameraHelper(webgl, p.cameraHelper);
|
||||
this.depthTarget = webgl.createRenderTarget(width, height);
|
||||
this.depthTexture = this.depthTarget.texture;
|
||||
|
||||
this.depthTargetPrimitives = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
|
||||
this.depthTargetVolumes = this.packedDepth ? webgl.createRenderTarget(width, height) : null;
|
||||
|
||||
this.depthTexturePrimitives = this.depthTargetPrimitives ? this.depthTargetPrimitives.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
|
||||
this.depthTextureVolumes = this.depthTargetVolumes ? this.depthTargetVolumes.texture : resources.texture('image-depth', 'depth', 'ushort', 'nearest');
|
||||
if (!this.packedDepth) {
|
||||
this.depthTexturePrimitives.define(width, height);
|
||||
this.depthTextureVolumes.define(width, height);
|
||||
}
|
||||
this.depthMerge = getDepthMergeRenderable(webgl, this.depthTexturePrimitives, this.depthTextureVolumes, this.packedDepth);
|
||||
|
||||
this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.colorTarget.setSize(width, height);
|
||||
if (this.depthTarget) {
|
||||
const w = this.colorTarget.getWidth();
|
||||
const h = this.colorTarget.getHeight();
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.colorTarget.setSize(width, height);
|
||||
this.depthTarget.setSize(width, height);
|
||||
} else {
|
||||
this.depthTexture.define(width, height);
|
||||
|
||||
if (this.depthTargetPrimitives) {
|
||||
this.depthTargetPrimitives.setSize(width, height);
|
||||
} else {
|
||||
this.depthTexturePrimitives.define(width, height);
|
||||
}
|
||||
|
||||
if (this.depthTargetVolumes) {
|
||||
this.depthTargetVolumes.setSize(width, height);
|
||||
} else {
|
||||
this.depthTextureVolumes.define(width, height);
|
||||
}
|
||||
|
||||
ValueCell.update(this.depthMerge.values.uTexSize, Vec2.set(this.depthMerge.values.uTexSize.ref.value, width, height));
|
||||
|
||||
if (this.wboit?.enabled) {
|
||||
this.wboit.setSize(width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setProps(props: Partial<DrawPassProps>) {
|
||||
if (props.cameraHelper) this.cameraHelper.setProps(props.cameraHelper);
|
||||
private _depthMerge() {
|
||||
const { state, gl } = this.webgl;
|
||||
|
||||
this.depthMerge.update();
|
||||
this.depthTarget.bind();
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.disable(gl.CULL_FACE);
|
||||
state.depthMask(false);
|
||||
state.clearColor(1, 1, 1, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
this.depthMerge.render();
|
||||
}
|
||||
|
||||
get props(): DrawPassProps {
|
||||
return {
|
||||
cameraHelper: { ...this.cameraHelper.props }
|
||||
};
|
||||
private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
|
||||
if (!this.wboit?.enabled) throw new Error('expected wboit to be enabled');
|
||||
|
||||
const renderTarget = toDrawingBuffer ? this.drawTarget : this.colorTarget;
|
||||
renderTarget.bind();
|
||||
renderer.clear(true);
|
||||
|
||||
// render opaque primitives
|
||||
this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
|
||||
renderTarget.bind();
|
||||
renderer.renderWboitOpaque(scene.primitives, camera, null);
|
||||
|
||||
// render opaque volumes
|
||||
this.depthTextureVolumes.attachFramebuffer(renderTarget.framebuffer, 'depth');
|
||||
renderTarget.bind();
|
||||
renderer.clearDepth();
|
||||
renderer.renderWboitOpaque(scene.volumes, camera, this.depthTexturePrimitives);
|
||||
|
||||
// merge depth of opaque primitives and volumes
|
||||
this._depthMerge();
|
||||
|
||||
// render transparent primitives and volumes
|
||||
this.wboit.bind();
|
||||
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTexture);
|
||||
renderer.renderWboitTransparent(scene.volumes, camera, this.depthTexture);
|
||||
|
||||
// evaluate wboit
|
||||
this.depthTexturePrimitives.attachFramebuffer(renderTarget.framebuffer, 'depth');
|
||||
renderTarget.bind();
|
||||
this.wboit.render();
|
||||
}
|
||||
|
||||
render(toDrawingBuffer: boolean, transparentBackground: boolean) {
|
||||
const { webgl, renderer, colorTarget, depthTarget } = this;
|
||||
private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean) {
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer();
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
colorTarget.bind();
|
||||
this.colorTarget.bind();
|
||||
if (!this.packedDepth) {
|
||||
// TODO unlcear why it is not enough to call `attachFramebuffer` in `Texture.reset`
|
||||
this.depthTexture.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
}
|
||||
}
|
||||
|
||||
renderer.setViewport(0, 0, colorTarget.getWidth(), colorTarget.getHeight());
|
||||
this.renderInternal('color', transparentBackground);
|
||||
renderer.clear(true);
|
||||
renderer.renderBlendedOpaque(scene.primitives, camera, null);
|
||||
|
||||
// do a depth pass if not rendering to drawing buffer and
|
||||
// extensions.depthTexture is unsupported (i.e. depthTarget is set)
|
||||
if (!toDrawingBuffer && depthTarget) {
|
||||
depthTarget.bind();
|
||||
this.renderInternal('depth', transparentBackground);
|
||||
if (!toDrawingBuffer && this.depthTargetPrimitives) {
|
||||
this.depthTargetPrimitives.bind();
|
||||
renderer.clear(false);
|
||||
renderer.renderDepth(scene.primitives, camera, null);
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
|
||||
// do direct-volume rendering
|
||||
if (!toDrawingBuffer) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureVolumes.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
renderer.clearDepth(); // from previous frame
|
||||
}
|
||||
renderer.renderBlendedVolume(scene.volumes, camera, this.depthTexturePrimitives);
|
||||
|
||||
// do volume depth pass if extensions.depthTexture is unsupported (i.e. depthTarget is set)
|
||||
if (this.depthTargetVolumes) {
|
||||
this.depthTargetVolumes.bind();
|
||||
renderer.clear(false);
|
||||
renderer.renderDepth(scene.volumes, camera, this.depthTexturePrimitives);
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
|
||||
if (!this.packedDepth) {
|
||||
this.depthTexturePrimitives.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
}
|
||||
}
|
||||
|
||||
renderer.renderBlendedTransparent(scene.primitives, camera, null);
|
||||
|
||||
// merge depths from primitive and volume rendering
|
||||
if (!toDrawingBuffer) {
|
||||
this._depthMerge();
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
}
|
||||
|
||||
private renderInternal(variant: 'color' | 'depth', transparentBackground: boolean) {
|
||||
const { renderer, scene, camera, debugHelper, cameraHelper, handleHelper } = this;
|
||||
renderer.render(scene, camera, variant, true, transparentBackground);
|
||||
if (debugHelper.isEnabled) {
|
||||
debugHelper.syncVisibility();
|
||||
renderer.render(debugHelper.scene, camera, variant, false, transparentBackground);
|
||||
private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean) {
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera);
|
||||
|
||||
if (this.wboitEnabled) {
|
||||
this._renderWboit(renderer, camera, scene, toDrawingBuffer);
|
||||
} else {
|
||||
this._renderBlended(renderer, camera, scene, toDrawingBuffer);
|
||||
}
|
||||
if (handleHelper.isEnabled) {
|
||||
renderer.render(handleHelper.scene, camera, variant, false, transparentBackground);
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera, null);
|
||||
}
|
||||
if (cameraHelper.isEnabled) {
|
||||
cameraHelper.update(camera);
|
||||
renderer.render(cameraHelper.scene, cameraHelper.camera, variant, false, transparentBackground);
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera, null);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
|
||||
}
|
||||
|
||||
this.webgl.gl.flush();
|
||||
}
|
||||
|
||||
render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean) {
|
||||
renderer.setTransparentBackground(transparentBackground);
|
||||
renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
|
||||
|
||||
if (StereoCamera.is(camera)) {
|
||||
this._render(renderer, camera.left, scene, helper, toDrawingBuffer);
|
||||
this._render(renderer, camera.right, scene, helper, toDrawingBuffer);
|
||||
} else {
|
||||
this._render(renderer, camera, scene, helper, toDrawingBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,49 +8,59 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { BoundingSphereHelper } from '../helper/bounding-sphere-helper';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { DrawPass, DrawPassParams } from './draw';
|
||||
import { DrawPass } from './draw';
|
||||
import { PostprocessingPass, PostprocessingParams } from './postprocessing';
|
||||
import { MultiSamplePass, MultiSampleParams } from './multi-sample';
|
||||
import { MultiSamplePass, MultiSampleParams, MultiSampleHelper } from './multi-sample';
|
||||
import { Camera } from '../camera';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { HandleHelper } from '../helper/handle-helper';
|
||||
import { PixelData } from '../../mol-util/image';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
|
||||
|
||||
export const ImageParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
drawPass: PD.Group(DrawPassParams),
|
||||
|
||||
cameraHelper: PD.Group(CameraHelperParams),
|
||||
};
|
||||
export type ImageProps = PD.Values<typeof ImageParams>
|
||||
|
||||
export class ImagePass {
|
||||
private _width = 1024
|
||||
private _height = 768
|
||||
private _width = 0
|
||||
private _height = 0
|
||||
private _camera = new Camera()
|
||||
private _transparentBackground = false
|
||||
|
||||
readonly props: ImageProps
|
||||
|
||||
private _colorTarget: RenderTarget
|
||||
get colorTarget() { return this._colorTarget; }
|
||||
|
||||
readonly drawPass: DrawPass
|
||||
private readonly postprocessing: PostprocessingPass
|
||||
private readonly multiSample: MultiSamplePass
|
||||
private readonly drawPass: DrawPass
|
||||
private readonly postprocessingPass: PostprocessingPass
|
||||
private readonly multiSamplePass: MultiSamplePass
|
||||
private readonly multiSampleHelper: MultiSampleHelper
|
||||
private readonly helper: Helper
|
||||
|
||||
get width() { return this._width; }
|
||||
get height() { return this._height; }
|
||||
|
||||
constructor(webgl: WebGLContext, private renderer: Renderer, scene: Scene, private camera: Camera, debugHelper: BoundingSphereHelper, handleHelper: HandleHelper, props: Partial<ImageProps>) {
|
||||
const p = { ...PD.getDefaultValues(ImageParams), ...props };
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) {
|
||||
this.props = { ...PD.getDefaultValues(ImageParams), ...props };
|
||||
|
||||
this._transparentBackground = p.transparentBackground;
|
||||
this.drawPass = new DrawPass(webgl, 128, 128, enableWboit);
|
||||
this.postprocessingPass = new PostprocessingPass(webgl, this.drawPass);
|
||||
this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass, this.postprocessingPass);
|
||||
this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
|
||||
|
||||
this.drawPass = new DrawPass(webgl, renderer, scene, this._camera, debugHelper, handleHelper, p.drawPass);
|
||||
this.postprocessing = new PostprocessingPass(webgl, this._camera, this.drawPass, p.postprocessing);
|
||||
this.multiSample = new MultiSamplePass(webgl, this._camera, this.drawPass, this.postprocessing, p.multiSample);
|
||||
this.helper = {
|
||||
camera: new CameraHelper(webgl, this.props.cameraHelper),
|
||||
debug: helper.debug,
|
||||
handle: helper.handle,
|
||||
};
|
||||
|
||||
this.setSize(this._width, this._height);
|
||||
this.setSize(1024, 768);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
@@ -60,24 +70,13 @@ export class ImagePass {
|
||||
this._height = height;
|
||||
|
||||
this.drawPass.setSize(width, height);
|
||||
this.postprocessing.setSize(width, height);
|
||||
this.multiSample.setSize(width, height);
|
||||
this.postprocessingPass.syncSize();
|
||||
this.multiSamplePass.syncSize();
|
||||
}
|
||||
|
||||
setProps(props: Partial<ImageProps> = {}) {
|
||||
if (props.transparentBackground !== undefined) this._transparentBackground = props.transparentBackground;
|
||||
if (props.postprocessing) this.postprocessing.setProps(props.postprocessing);
|
||||
if (props.multiSample) this.multiSample.setProps(props.multiSample);
|
||||
if (props.drawPass) this.drawPass.setProps(props.drawPass);
|
||||
}
|
||||
|
||||
get props(): ImageProps {
|
||||
return {
|
||||
transparentBackground: this._transparentBackground,
|
||||
postprocessing: this.postprocessing.props,
|
||||
multiSample: this.multiSample.props,
|
||||
drawPass: this.drawPass.props
|
||||
};
|
||||
Object.assign(this.props, props);
|
||||
if (props.cameraHelper) this.helper.camera.setProps(props.cameraHelper);
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -85,26 +84,34 @@ export class ImagePass {
|
||||
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height);
|
||||
this._camera.update();
|
||||
|
||||
this.renderer.setViewport(0, 0, this._width, this._height);
|
||||
|
||||
if (this.multiSample.enabled) {
|
||||
this.multiSample.render(false, this._transparentBackground);
|
||||
this._colorTarget = this.multiSample.colorTarget;
|
||||
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
|
||||
this.multiSampleHelper.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground, this.props);
|
||||
this._colorTarget = this.multiSamplePass.colorTarget;
|
||||
} else {
|
||||
this.drawPass.render(false, this._transparentBackground);
|
||||
if (this.postprocessing.enabled) {
|
||||
this.postprocessing.render(false);
|
||||
this._colorTarget = this.postprocessing.target;
|
||||
this.drawPass.render(this.renderer, this._camera, this.scene, this.helper, false, this.props.transparentBackground);
|
||||
if (PostprocessingPass.isEnabled(this.props.postprocessing)) {
|
||||
this.postprocessingPass.render(this._camera, false, this.props.postprocessing);
|
||||
this._colorTarget = this.postprocessingPass.target;
|
||||
} else {
|
||||
this._colorTarget = this.drawPass.colorTarget;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(width: number, height: number) {
|
||||
getImageData(width: number, height: number, viewport?: Viewport) {
|
||||
this.setSize(width, height);
|
||||
this.render();
|
||||
const pd = this.colorTarget.getPixelData();
|
||||
return new ImageData(new Uint8ClampedArray(pd.array), pd.width, pd.height);
|
||||
this.colorTarget.bind();
|
||||
|
||||
const w = viewport?.width ?? width, h = viewport?.height ?? height;
|
||||
|
||||
const array = new Uint8Array(w * h * 4);
|
||||
if (!viewport) {
|
||||
this.webgl.readPixels(0, 0, w, h, array);
|
||||
} else {
|
||||
this.webgl.readPixels(viewport.x, height - viewport.y - viewport.height, w, h, array);
|
||||
}
|
||||
PixelData.flipY({ array, width: w, height: h });
|
||||
return new ImageData(new Uint8ClampedArray(array), w, h);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -16,8 +16,12 @@ import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/rendera
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { PostprocessingPass } from './postprocessing';
|
||||
import { PostprocessingPass, PostprocessingProps } from './postprocessing';
|
||||
import { DrawPass } from './draw';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
|
||||
import quad_vert from '../../mol-gl/shader/quad.vert';
|
||||
import compose_frag from '../../mol-gl/shader/compose.frag';
|
||||
@@ -28,7 +32,7 @@ const ComposeSchema = {
|
||||
uTexSize: UniformSpec('v2'),
|
||||
uWeight: UniformSpec('f'),
|
||||
};
|
||||
|
||||
const ComposeShaderCode = ShaderCode('compose', quad_vert, compose_frag);
|
||||
type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
|
||||
|
||||
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): ComposeRenderable {
|
||||
@@ -40,8 +44,7 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture): Compose
|
||||
};
|
||||
|
||||
const schema = { ...ComposeSchema };
|
||||
const shaderCode = ShaderCode('compose', quad_vert, compose_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', ComposeShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -52,69 +55,61 @@ export const MultiSampleParams = {
|
||||
};
|
||||
export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
|
||||
|
||||
type Props = { multiSample: MultiSampleProps, postprocessing: PostprocessingProps }
|
||||
|
||||
export class MultiSamplePass {
|
||||
props: MultiSampleProps
|
||||
static isEnabled(props: MultiSampleProps) {
|
||||
return props.mode !== 'off';
|
||||
}
|
||||
|
||||
colorTarget: RenderTarget
|
||||
|
||||
private composeTarget: RenderTarget
|
||||
private holdTarget: RenderTarget
|
||||
private compose: ComposeRenderable
|
||||
|
||||
private sampleIndex = -1
|
||||
private currentTime = 0
|
||||
private lastRenderTime = 0
|
||||
|
||||
constructor(private webgl: WebGLContext, private camera: Camera, private drawPass: DrawPass, private postprocessing: PostprocessingPass, props: Partial<MultiSampleProps>) {
|
||||
const { gl } = webgl;
|
||||
this.colorTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
this.composeTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
this.holdTarget = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
constructor(private webgl: WebGLContext, private drawPass: DrawPass, private postprocessing: PostprocessingPass) {
|
||||
const { colorBufferFloat, textureFloat } = webgl.extensions;
|
||||
const width = drawPass.colorTarget.getWidth();
|
||||
const height = drawPass.colorTarget.getHeight();
|
||||
this.colorTarget = webgl.createRenderTarget(width, height, false);
|
||||
this.composeTarget = webgl.createRenderTarget(width, height, false, colorBufferFloat && textureFloat ? 'float32' : 'uint8');
|
||||
this.holdTarget = webgl.createRenderTarget(width, height, false);
|
||||
this.compose = getComposeRenderable(webgl, drawPass.colorTarget.texture);
|
||||
this.props = { ...PD.getDefaultValues(MultiSampleParams), ...props };
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
if (this.props.mode === 'temporal') {
|
||||
if (this.currentTime - this.lastRenderTime > 200) {
|
||||
return this.sampleIndex !== -1;
|
||||
} else {
|
||||
this.sampleIndex = 0;
|
||||
return false;
|
||||
}
|
||||
} else if (this.props.mode === 'on') {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
syncSize() {
|
||||
const width = this.drawPass.colorTarget.getWidth();
|
||||
const height = this.drawPass.colorTarget.getHeight();
|
||||
|
||||
const [w, h] = this.compose.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
this.colorTarget.setSize(width, height);
|
||||
this.composeTarget.setSize(width, height);
|
||||
this.holdTarget.setSize(width, height);
|
||||
ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
update(changed: boolean, currentTime: number) {
|
||||
if (changed) this.lastRenderTime = currentTime;
|
||||
this.currentTime = currentTime;
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.colorTarget.setSize(width, height);
|
||||
this.composeTarget.setSize(width, height);
|
||||
this.holdTarget.setSize(width, height);
|
||||
ValueCell.update(this.compose.values.uTexSize, Vec2.set(this.compose.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
|
||||
setProps(props: Partial<MultiSampleProps>) {
|
||||
if (props.mode !== undefined) this.props.mode = props.mode;
|
||||
if (props.sampleLevel !== undefined) this.props.sampleLevel = props.sampleLevel;
|
||||
}
|
||||
|
||||
render(toDrawingBuffer: boolean, transparentBackground: boolean) {
|
||||
if (this.props.mode === 'temporal') {
|
||||
this.renderTemporalMultiSample(toDrawingBuffer, transparentBackground);
|
||||
render(sampleIndex: number, renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
|
||||
if (props.multiSample.mode === 'temporal') {
|
||||
return this.renderTemporalMultiSample(sampleIndex, renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
|
||||
} else {
|
||||
this.renderMultiSample(toDrawingBuffer, transparentBackground);
|
||||
this.renderMultiSample(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
|
||||
return sampleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private renderMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) {
|
||||
const { camera, compose, composeTarget, drawPass, postprocessing, webgl } = this;
|
||||
private bindOutputTarget(toDrawingBuffer: boolean) {
|
||||
if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
}
|
||||
|
||||
private renderMultiSample(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
|
||||
const { compose, composeTarget, drawPass, postprocessing, webgl } = this;
|
||||
const { gl, state } = webgl;
|
||||
|
||||
// based on the Multisample Anti-Aliasing Render Pass
|
||||
@@ -122,17 +117,17 @@ export class MultiSamplePass {
|
||||
//
|
||||
// This manual approach to MSAA re-renders the scene once for
|
||||
// each sample with camera jitter and accumulates the results.
|
||||
const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ];
|
||||
const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
const baseSampleWeight = 1.0 / offsetList.length;
|
||||
const roundingRange = 1 / 32;
|
||||
|
||||
camera.viewOffset.enabled = true;
|
||||
ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
compose.update();
|
||||
const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
|
||||
|
||||
const width = drawPass.colorTarget.getWidth();
|
||||
const height = drawPass.colorTarget.getHeight();
|
||||
camera.viewOffset.enabled = true;
|
||||
ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
compose.update();
|
||||
|
||||
// render the scene multiple times, each slightly jitter offset
|
||||
// from the last and accumulate the results.
|
||||
@@ -149,18 +144,18 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, sampleWeight);
|
||||
|
||||
// render scene and optionally postprocess
|
||||
drawPass.render(false, transparentBackground);
|
||||
if (postprocessing.enabled) postprocessing.render(false);
|
||||
drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
|
||||
if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
|
||||
|
||||
// compose rendered scene with compose target
|
||||
composeTarget.bind();
|
||||
gl.viewport(0, 0, width, height);
|
||||
state.enable(gl.BLEND);
|
||||
state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
||||
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.disable(gl.SCISSOR_TEST);
|
||||
state.depthMask(false);
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
if (i === 0) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
@@ -172,12 +167,10 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture);
|
||||
compose.update();
|
||||
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
gl.viewport(0, 0, width, height);
|
||||
this.bindOutputTarget(toDrawingBuffer);
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
|
||||
state.disable(gl.BLEND);
|
||||
compose.render();
|
||||
|
||||
@@ -185,8 +178,8 @@ export class MultiSamplePass {
|
||||
camera.update();
|
||||
}
|
||||
|
||||
private renderTemporalMultiSample(toDrawingBuffer: boolean, transparentBackground: boolean) {
|
||||
const { camera, compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
|
||||
private renderTemporalMultiSample(sampleIndex: number, renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
|
||||
const { compose, composeTarget, holdTarget, postprocessing, drawPass, webgl } = this;
|
||||
const { gl, state } = webgl;
|
||||
|
||||
// based on the Multisample Anti-Aliasing Render Pass
|
||||
@@ -194,79 +187,76 @@ export class MultiSamplePass {
|
||||
//
|
||||
// This manual approach to MSAA re-renders the scene once for
|
||||
// each sample with camera jitter and accumulates the results.
|
||||
const offsetList = JitterVectors[ Math.max(0, Math.min(this.props.sampleLevel, 5)) ];
|
||||
const offsetList = JitterVectors[ Math.max(0, Math.min(props.multiSample.sampleLevel, 5)) ];
|
||||
|
||||
if (this.sampleIndex === -1) return;
|
||||
if (this.sampleIndex >= offsetList.length) {
|
||||
this.sampleIndex = -1;
|
||||
return;
|
||||
}
|
||||
if (sampleIndex === -2 || sampleIndex >= offsetList.length) return -2;
|
||||
|
||||
const i = this.sampleIndex;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
const sampleWeight = 1.0 / offsetList.length;
|
||||
const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
|
||||
|
||||
if (i === 0) {
|
||||
drawPass.render(false, transparentBackground);
|
||||
if (postprocessing.enabled) postprocessing.render(false);
|
||||
if (sampleIndex === -1) {
|
||||
drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
|
||||
if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
|
||||
ValueCell.update(compose.values.uWeight, 1.0);
|
||||
ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
compose.update();
|
||||
|
||||
holdTarget.bind();
|
||||
state.disable(gl.BLEND);
|
||||
compose.render();
|
||||
}
|
||||
|
||||
const sampleWeight = 1.0 / offsetList.length;
|
||||
|
||||
camera.viewOffset.enabled = true;
|
||||
ValueCell.update(compose.values.tColor, postprocessing.enabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
ValueCell.update(compose.values.uWeight, sampleWeight);
|
||||
compose.update();
|
||||
|
||||
const width = drawPass.colorTarget.getWidth();
|
||||
const height = drawPass.colorTarget.getHeight();
|
||||
|
||||
// render the scene multiple times, each slightly jitter offset
|
||||
// from the last and accumulate the results.
|
||||
const numSamplesPerFrame = Math.pow(2, this.props.sampleLevel);
|
||||
for (let i = 0; i < numSamplesPerFrame; ++i) {
|
||||
const offset = offsetList[this.sampleIndex];
|
||||
Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
|
||||
camera.update();
|
||||
|
||||
// render scene and optionally postprocess
|
||||
drawPass.render(false, transparentBackground);
|
||||
if (postprocessing.enabled) postprocessing.render(false);
|
||||
|
||||
// compose rendered scene with compose target
|
||||
composeTarget.bind();
|
||||
state.enable(gl.BLEND);
|
||||
state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
||||
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.disable(gl.SCISSOR_TEST);
|
||||
state.depthMask(false);
|
||||
if (this.sampleIndex === 0) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
compose.render();
|
||||
sampleIndex += 1;
|
||||
} else {
|
||||
camera.viewOffset.enabled = true;
|
||||
ValueCell.update(compose.values.tColor, postprocessingEnabled ? postprocessing.target.texture : drawPass.colorTarget.texture);
|
||||
ValueCell.update(compose.values.uWeight, sampleWeight);
|
||||
compose.update();
|
||||
|
||||
this.sampleIndex += 1;
|
||||
if (this.sampleIndex >= offsetList.length ) break;
|
||||
// render the scene multiple times, each slightly jitter offset
|
||||
// from the last and accumulate the results.
|
||||
const numSamplesPerFrame = Math.pow(2, Math.max(0, props.multiSample.sampleLevel - 2));
|
||||
for (let i = 0; i < numSamplesPerFrame; ++i) {
|
||||
const offset = offsetList[sampleIndex];
|
||||
Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
|
||||
camera.update();
|
||||
|
||||
// render scene and optionally postprocess
|
||||
drawPass.render(renderer, camera, scene, helper, false, transparentBackground);
|
||||
if (postprocessingEnabled) postprocessing.render(camera, false, props.postprocessing);
|
||||
|
||||
// compose rendered scene with compose target
|
||||
composeTarget.bind();
|
||||
state.enable(gl.BLEND);
|
||||
state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
||||
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
if (sampleIndex === 0) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
compose.render();
|
||||
|
||||
sampleIndex += 1;
|
||||
if (sampleIndex >= offsetList.length ) break;
|
||||
}
|
||||
}
|
||||
|
||||
const accumulationWeight = this.sampleIndex * sampleWeight;
|
||||
this.bindOutputTarget(toDrawingBuffer);
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
|
||||
const accumulationWeight = sampleIndex * sampleWeight;
|
||||
if (accumulationWeight > 0) {
|
||||
ValueCell.update(compose.values.uWeight, 1.0);
|
||||
ValueCell.update(compose.values.tColor, composeTarget.texture);
|
||||
compose.update();
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
gl.viewport(0, 0, width, height);
|
||||
state.disable(gl.BLEND);
|
||||
compose.render();
|
||||
}
|
||||
@@ -274,12 +264,6 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, 1.0 - accumulationWeight);
|
||||
ValueCell.update(compose.values.tColor, holdTarget.texture);
|
||||
compose.update();
|
||||
if (toDrawingBuffer) {
|
||||
webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
gl.viewport(0, 0, width, height);
|
||||
if (accumulationWeight === 0) state.disable(gl.BLEND);
|
||||
else state.enable(gl.BLEND);
|
||||
compose.render();
|
||||
@@ -287,7 +271,8 @@ export class MultiSamplePass {
|
||||
|
||||
camera.viewOffset.enabled = false;
|
||||
camera.update();
|
||||
if (this.sampleIndex >= offsetList.length) this.sampleIndex = -1;
|
||||
|
||||
return sampleIndex >= offsetList.length ? -2 : sampleIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,4 +314,21 @@ JitterVectors.forEach(offsetList => {
|
||||
offset[0] *= 0.0625;
|
||||
offset[1] *= 0.0625;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export class MultiSampleHelper {
|
||||
private sampleIndex = -2
|
||||
|
||||
update(changed: boolean, props: MultiSampleProps) {
|
||||
if (changed) this.sampleIndex = -1;
|
||||
return props.mode === 'temporal' ? this.sampleIndex !== -2 : false;
|
||||
}
|
||||
|
||||
render(renderer: Renderer, camera: Camera | StereoCamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
|
||||
this.sampleIndex = this.multiSamplePass.render(this.sampleIndex, renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
|
||||
}
|
||||
|
||||
constructor(private multiSamplePass: MultiSamplePass) {
|
||||
|
||||
}
|
||||
}
|
||||
34
src/mol-canvas3d/passes/passes.ts
Normal file
34
src/mol-canvas3d/passes/passes.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { DrawPass } from './draw';
|
||||
import { PickPass } from './pick';
|
||||
import { PostprocessingPass } from './postprocessing';
|
||||
import { MultiSamplePass } from './multi-sample';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
|
||||
export class Passes {
|
||||
readonly draw: DrawPass
|
||||
readonly pick: PickPass
|
||||
readonly postprocessing: PostprocessingPass
|
||||
readonly multiSample: MultiSamplePass
|
||||
|
||||
constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
|
||||
const { gl } = webgl;
|
||||
this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
|
||||
this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
|
||||
this.postprocessing = new PostprocessingPass(webgl, this.draw);
|
||||
this.multiSample = new MultiSamplePass(webgl, this.draw, this.postprocessing);
|
||||
}
|
||||
|
||||
updateSize() {
|
||||
const { gl } = this.webgl;
|
||||
this.draw.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
this.pick.syncSize();
|
||||
this.postprocessing.syncSize();
|
||||
this.multiSample.syncSize();
|
||||
}
|
||||
}
|
||||
@@ -1,131 +1,251 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import Renderer from '../../mol-gl/renderer';
|
||||
import Scene from '../../mol-gl/scene';
|
||||
import { PickingId } from '../../mol-geo/geometry/picking';
|
||||
import { decodeFloatRGB } from '../../mol-util/float-packing';
|
||||
import { Camera } from '../camera';
|
||||
import { HandleHelper } from '../helper/handle-helper';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { GraphicsRenderVariant } from '../../mol-gl/webgl/render-item';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { decodeFloatRGB, unpackRGBAToDepth } from '../../mol-util/float-packing';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
import { cameraUnproject } from '../camera/util';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { DrawPass } from './draw';
|
||||
|
||||
const NullId = Math.pow(2, 24) - 2;
|
||||
|
||||
export type PickData = { id: PickingId, position: Vec3 }
|
||||
|
||||
export class PickPass {
|
||||
pickDirty = true
|
||||
readonly objectPickTarget: RenderTarget
|
||||
readonly instancePickTarget: RenderTarget
|
||||
readonly groupPickTarget: RenderTarget
|
||||
readonly depthPickTarget: RenderTarget
|
||||
|
||||
objectPickTarget: RenderTarget
|
||||
instancePickTarget: RenderTarget
|
||||
groupPickTarget: RenderTarget
|
||||
|
||||
private objectBuffer: Uint8Array
|
||||
private instanceBuffer: Uint8Array
|
||||
private groupBuffer: Uint8Array
|
||||
|
||||
private pickScale: number
|
||||
private pickWidth: number
|
||||
private pickHeight: number
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, private handleHelper: HandleHelper, private pickBaseScale: number) {
|
||||
const { gl } = webgl;
|
||||
const width = gl.drawingBufferWidth;
|
||||
const height = gl.drawingBufferHeight;
|
||||
|
||||
this.pickScale = pickBaseScale / webgl.pixelRatio;
|
||||
this.pickWidth = Math.round(width * this.pickScale);
|
||||
this.pickHeight = Math.round(height * this.pickScale);
|
||||
constructor(private webgl: WebGLContext, private drawPass: DrawPass, readonly pickBaseScale: number) {
|
||||
const pickScale = pickBaseScale / webgl.pixelRatio;
|
||||
this.pickWidth = Math.ceil(drawPass.colorTarget.getWidth() * pickScale);
|
||||
this.pickHeight = Math.ceil(drawPass.colorTarget.getHeight() * pickScale);
|
||||
|
||||
this.objectPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
|
||||
this.instancePickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
|
||||
this.groupPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
|
||||
|
||||
this.setupBuffers();
|
||||
this.depthPickTarget = webgl.createRenderTarget(this.pickWidth, this.pickHeight);
|
||||
}
|
||||
|
||||
get drawingBufferHeight() {
|
||||
return this.drawPass.colorTarget.getHeight();
|
||||
}
|
||||
|
||||
syncSize() {
|
||||
const pickScale = this.pickBaseScale / this.webgl.pixelRatio;
|
||||
const pickWidth = Math.ceil(this.drawPass.colorTarget.getWidth() * pickScale);
|
||||
const pickHeight = Math.ceil(this.drawPass.colorTarget.getHeight() * pickScale);
|
||||
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
this.pickHeight = pickHeight;
|
||||
|
||||
this.objectPickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.instancePickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.groupPickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.depthPickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
}
|
||||
}
|
||||
|
||||
private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: GraphicsRenderVariant) {
|
||||
const depth = this.drawPass.depthTexturePrimitives;
|
||||
renderer.clear(false);
|
||||
renderer.renderPick(scene.primitives, camera, variant, null);
|
||||
renderer.renderPick(scene.volumes, camera, variant, depth);
|
||||
renderer.renderPick(helper.handle.scene, camera, variant, null);
|
||||
}
|
||||
|
||||
render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper) {
|
||||
renderer.update(camera);
|
||||
|
||||
this.objectPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pickObject');
|
||||
|
||||
this.instancePickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pickInstance');
|
||||
|
||||
this.groupPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'pickGroup');
|
||||
|
||||
this.depthPickTarget.bind();
|
||||
this.renderVariant(renderer, camera, scene, helper, 'depth');
|
||||
}
|
||||
}
|
||||
|
||||
export class PickHelper {
|
||||
dirty = true
|
||||
|
||||
private objectBuffer: Uint8Array
|
||||
private instanceBuffer: Uint8Array
|
||||
private groupBuffer: Uint8Array
|
||||
private depthBuffer: Uint8Array
|
||||
|
||||
private viewport = Viewport()
|
||||
|
||||
private pickScale: number
|
||||
private pickX: number
|
||||
private pickY: number
|
||||
private pickWidth: number
|
||||
private pickHeight: number
|
||||
private halfPickWidth: number
|
||||
|
||||
private setupBuffers() {
|
||||
const bufferSize = this.pickWidth * this.pickHeight * 4;
|
||||
if (!this.objectBuffer || this.objectBuffer.length !== bufferSize) {
|
||||
this.objectBuffer = new Uint8Array(bufferSize);
|
||||
this.instanceBuffer = new Uint8Array(bufferSize);
|
||||
this.groupBuffer = new Uint8Array(bufferSize);
|
||||
this.depthBuffer = new Uint8Array(bufferSize);
|
||||
}
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.pickScale = this.pickBaseScale / this.webgl.pixelRatio;
|
||||
this.pickWidth = Math.round(width * this.pickScale);
|
||||
this.pickHeight = Math.round(height * this.pickScale);
|
||||
setViewport(x: number, y: number, width: number, height: number) {
|
||||
Viewport.set(this.viewport, x, y, width, height);
|
||||
|
||||
this.objectPickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.instancePickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.groupPickTarget.setSize(this.pickWidth, this.pickHeight);
|
||||
this.pickScale = this.pickPass.pickBaseScale / this.webgl.pixelRatio;
|
||||
this.pickX = Math.ceil(x * this.pickScale);
|
||||
this.pickY = Math.ceil(y * this.pickScale);
|
||||
|
||||
this.setupBuffers();
|
||||
}
|
||||
const pickWidth = Math.ceil(width * this.pickScale);
|
||||
const pickHeight = Math.ceil(height * this.pickScale);
|
||||
|
||||
render() {
|
||||
const { renderer, scene, camera, handleHelper: { scene: handleScene } } = this;
|
||||
renderer.setViewport(0, 0, this.pickWidth, this.pickHeight);
|
||||
if (pickWidth !== this.pickWidth || pickHeight !== this.pickHeight) {
|
||||
this.pickWidth = pickWidth;
|
||||
this.pickHeight = pickHeight;
|
||||
this.halfPickWidth = Math.floor(this.pickWidth / 2);
|
||||
|
||||
this.objectPickTarget.bind();
|
||||
renderer.render(scene, camera, 'pickObject', true, false);
|
||||
renderer.render(handleScene, camera, 'pickObject', false, false);
|
||||
this.instancePickTarget.bind();
|
||||
renderer.render(scene, camera, 'pickInstance', true, false);
|
||||
renderer.render(handleScene, camera, 'pickInstance', false, false);
|
||||
this.groupPickTarget.bind();
|
||||
renderer.render(scene, camera, 'pickGroup', true, false);
|
||||
renderer.render(handleScene, camera, 'pickGroup', false, false);
|
||||
|
||||
this.pickDirty = false;
|
||||
this.setupBuffers();
|
||||
}
|
||||
}
|
||||
|
||||
private syncBuffers() {
|
||||
const { webgl } = this;
|
||||
const { pickX, pickY, pickWidth, pickHeight } = this;
|
||||
|
||||
this.objectPickTarget.bind();
|
||||
webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.objectBuffer);
|
||||
this.pickPass.objectPickTarget.bind();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.objectBuffer);
|
||||
|
||||
this.instancePickTarget.bind();
|
||||
webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.instanceBuffer);
|
||||
this.pickPass.instancePickTarget.bind();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.instanceBuffer);
|
||||
|
||||
this.groupPickTarget.bind();
|
||||
webgl.readPixels(0, 0, this.pickWidth, this.pickHeight, this.groupBuffer);
|
||||
this.pickPass.groupPickTarget.bind();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.groupBuffer);
|
||||
|
||||
this.pickPass.depthPickTarget.bind();
|
||||
this.webgl.readPixels(pickX, pickY, pickWidth, pickHeight, this.depthBuffer);
|
||||
}
|
||||
|
||||
private getBufferIdx(x: number, y: number): number {
|
||||
return (y * this.pickWidth + x) * 4;
|
||||
}
|
||||
|
||||
private getDepth(x: number, y: number): number {
|
||||
const idx = this.getBufferIdx(x, y);
|
||||
const b = this.depthBuffer;
|
||||
return unpackRGBAToDepth(b[idx], b[idx + 1], b[idx + 2], b[idx + 3]);
|
||||
}
|
||||
|
||||
private getId(x: number, y: number, buffer: Uint8Array) {
|
||||
const idx = (y * this.pickWidth + x) * 4;
|
||||
const idx = this.getBufferIdx(x, y);
|
||||
return decodeFloatRGB(buffer[idx], buffer[idx + 1], buffer[idx + 2]);
|
||||
}
|
||||
|
||||
identify(x: number, y: number): PickingId | undefined {
|
||||
private render(camera: Camera | StereoCamera) {
|
||||
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
|
||||
const { renderer, scene, helper } = this;
|
||||
|
||||
renderer.setTransparentBackground(false);
|
||||
renderer.setDrawingBufferSize(this.pickPass.objectPickTarget.getWidth(), this.pickPass.objectPickTarget.getHeight());
|
||||
|
||||
if (StereoCamera.is(camera)) {
|
||||
renderer.setViewport(pickX, pickY, halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.left, scene, helper);
|
||||
|
||||
renderer.setViewport(pickX + halfPickWidth, pickY, pickWidth - halfPickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera.right, scene, helper);
|
||||
} else {
|
||||
renderer.setViewport(pickX, pickY, pickWidth, pickHeight);
|
||||
this.pickPass.render(renderer, camera, scene, helper);
|
||||
}
|
||||
|
||||
this.dirty = false;
|
||||
}
|
||||
|
||||
identify(x: number, y: number, camera: Camera | StereoCamera): PickData | undefined {
|
||||
const { webgl, pickScale } = this;
|
||||
if (webgl.isContextLost) return;
|
||||
|
||||
const { gl } = webgl;
|
||||
if (this.pickDirty) {
|
||||
this.render();
|
||||
x *= webgl.pixelRatio;
|
||||
y *= webgl.pixelRatio;
|
||||
y = this.pickPass.drawingBufferHeight - y; // flip y
|
||||
|
||||
const { viewport } = this;
|
||||
|
||||
// check if within viewport
|
||||
if (x < viewport.x ||
|
||||
y < viewport.y ||
|
||||
x > viewport.x + viewport.width ||
|
||||
y > viewport.y + viewport.height
|
||||
) return;
|
||||
|
||||
if (this.dirty) {
|
||||
this.render(camera);
|
||||
this.syncBuffers();
|
||||
}
|
||||
|
||||
x *= webgl.pixelRatio;
|
||||
y *= webgl.pixelRatio;
|
||||
y = gl.drawingBufferHeight - y; // flip y
|
||||
const xv = x - viewport.x;
|
||||
const yv = y - viewport.y;
|
||||
|
||||
const xp = Math.round(x * pickScale);
|
||||
const yp = Math.round(y * pickScale);
|
||||
const xp = Math.floor(xv * pickScale);
|
||||
const yp = Math.floor(yv * pickScale);
|
||||
|
||||
const objectId = this.getId(xp, yp, this.objectBuffer);
|
||||
if (objectId === -1) return;
|
||||
// console.log('objectId', objectId);
|
||||
if (objectId === -1 || objectId === NullId) return;
|
||||
|
||||
const instanceId = this.getId(xp, yp, this.instanceBuffer);
|
||||
if (instanceId === -1) return;
|
||||
// console.log('instanceId', instanceId);
|
||||
if (instanceId === -1 || instanceId === NullId) return;
|
||||
|
||||
const groupId = this.getId(xp, yp, this.groupBuffer);
|
||||
if (groupId === -1) return;
|
||||
// console.log('groupId', groupId);
|
||||
if (groupId === -1 || groupId === NullId) return;
|
||||
|
||||
return { objectId, instanceId, groupId };
|
||||
const z = this.getDepth(xp, yp);
|
||||
const position = Vec3.create(x, viewport.height - y, z);
|
||||
if (StereoCamera.is(camera)) {
|
||||
const halfWidth = Math.floor(viewport.width / 2);
|
||||
if (x > viewport.x + halfWidth) {
|
||||
position[0] = viewport.x + (xv - halfWidth) * 2;
|
||||
cameraUnproject(position, position, viewport, camera.right.inverseProjectionView);
|
||||
} else {
|
||||
position[0] = viewport.x + xv * 2;
|
||||
cameraUnproject(position, position, viewport, camera.left.inverseProjectionView);
|
||||
}
|
||||
} else {
|
||||
cameraUnproject(position, position, viewport, camera.inverseProjectionView);
|
||||
}
|
||||
|
||||
// console.log({ { objectId, instanceId, groupId }, position} );
|
||||
return { id: { objectId, instanceId, groupId }, position };
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private helper: Helper, private pickPass: PickPass, viewport: Viewport) {
|
||||
this.setViewport(viewport.x, viewport.y, viewport.width, viewport.height);
|
||||
}
|
||||
}
|
||||
@@ -16,16 +16,16 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { DrawPass } from './draw';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { produce } from 'immer';
|
||||
|
||||
import { Camera, ICamera } from '../../mol-canvas3d/camera';
|
||||
import quad_vert from '../../mol-gl/shader/quad.vert';
|
||||
import postprocessing_frag from '../../mol-gl/shader/postprocessing.frag';
|
||||
import fxaa_frag from '../../mol-gl/shader/fxaa.frag';
|
||||
import { StereoCamera } from '../camera/stereo';
|
||||
|
||||
const PostprocessingSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tPackedDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
@@ -43,9 +43,42 @@ const PostprocessingSchema = {
|
||||
dOutlineEnable: DefineSpec('boolean'),
|
||||
uOutlineScale: UniformSpec('f'),
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
|
||||
dPackedDepth: DefineSpec('boolean'),
|
||||
};
|
||||
const PostprocessingShaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
|
||||
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
|
||||
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture): PostprocessingRenderable {
|
||||
const width = colorTexture.getWidth();
|
||||
const height = colorTexture.getHeight();
|
||||
|
||||
const values: Values<typeof PostprocessingSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tPackedDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(10000),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
dOcclusionEnable: ValueCell.create(false),
|
||||
dOcclusionKernelSize: ValueCell.create(4),
|
||||
uOcclusionBias: ValueCell.create(0.5),
|
||||
uOcclusionRadius: ValueCell.create(64),
|
||||
|
||||
dOutlineEnable: ValueCell.create(false),
|
||||
uOutlineScale: ValueCell.create(1 * ctx.pixelRatio),
|
||||
uOutlineThreshold: ValueCell.create(0.8),
|
||||
};
|
||||
|
||||
const schema = { ...PostprocessingSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', PostprocessingShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
export const PostprocessingParams = {
|
||||
occlusion: PD.MappedStatic('off', {
|
||||
@@ -62,113 +95,209 @@ export const PostprocessingParams = {
|
||||
threshold: PD.Numeric(0.8, { min: 0, max: 5, step: 0.01 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' })
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' }),
|
||||
antialiasing: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
edgeThresholdMin:PD.Numeric(0.0312, { min: 0.0312, max: 0.0833, step: 0.0001 }, { description: 'Trims the algorithm from processing darks.' }),
|
||||
edgeThresholdMax: PD.Numeric(0.063, { min: 0.063, max: 0.333, step: 0.001 }, { description: 'The minimum amount of local contrast required to apply algorithm.' }),
|
||||
iterations: PD.Numeric(12, { min: 0, max: 32, step: 1 }, { description: 'Number of edge exploration steps.' }),
|
||||
subpixelQuality: PD.Numeric(1.00, { min: 0.00, max: 1.00, step: 0.01 }, { description: 'Choose the amount of sub-pixel aliasing removal.' }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Fast Approximate Anti-Aliasing (FXAA)' }),
|
||||
};
|
||||
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
|
||||
|
||||
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
|
||||
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTexture: Texture, packedDepth: boolean, props: Partial<PostprocessingProps>): PostprocessingRenderable {
|
||||
const p = { ...PD.getDefaultValues(PostprocessingParams), ...props };
|
||||
const values: Values<typeof PostprocessingSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(10000),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
dOcclusionEnable: ValueCell.create(p.occlusion.name === 'on'),
|
||||
dOcclusionKernelSize: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.kernelSize : 4),
|
||||
uOcclusionBias: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.bias : 0.5),
|
||||
uOcclusionRadius: ValueCell.create(p.occlusion.name === 'on' ? p.occlusion.params.radius : 64),
|
||||
|
||||
dOutlineEnable: ValueCell.create(p.outline.name === 'on'),
|
||||
uOutlineScale: ValueCell.create((p.outline.name === 'on' ? p.outline.params.scale : 1) * ctx.pixelRatio),
|
||||
uOutlineThreshold: ValueCell.create(p.outline.name === 'on' ? p.outline.params.threshold : 0.8),
|
||||
|
||||
dPackedDepth: ValueCell.create(packedDepth),
|
||||
};
|
||||
|
||||
const schema = { ...PostprocessingSchema };
|
||||
const shaderCode = ShaderCode('postprocessing', quad_vert, postprocessing_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
export class PostprocessingPass {
|
||||
target: RenderTarget
|
||||
props: PostprocessingProps
|
||||
renderable: PostprocessingRenderable
|
||||
|
||||
constructor(private webgl: WebGLContext, private camera: Camera, drawPass: DrawPass, props: Partial<PostprocessingProps>) {
|
||||
const { gl } = webgl;
|
||||
this.target = webgl.createRenderTarget(gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
this.props = { ...PD.getDefaultValues(PostprocessingParams), ...props };
|
||||
const { colorTarget, depthTexture, packedDepth } = drawPass;
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture, packedDepth, this.props);
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name === 'on';
|
||||
}
|
||||
|
||||
get enabled() {
|
||||
return this.props.occlusion.name === 'on' || this.props.outline.name === 'on';
|
||||
readonly target: RenderTarget
|
||||
|
||||
private readonly tmpTarget: RenderTarget
|
||||
private readonly renderable: PostprocessingRenderable
|
||||
private readonly fxaa: FxaaRenderable
|
||||
|
||||
constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
|
||||
const { colorTarget, depthTexture } = drawPass;
|
||||
const width = colorTarget.getWidth();
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
this.tmpTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTexture);
|
||||
this.fxaa = getFxaaRenderable(webgl, this.tmpTarget.texture);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
this.target.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
syncSize() {
|
||||
const width = this.drawPass.colorTarget.getWidth();
|
||||
const height = this.drawPass.colorTarget.getHeight();
|
||||
|
||||
setProps(props: Partial<PostprocessingProps>) {
|
||||
this.props = produce(this.props, p => {
|
||||
if (props.occlusion !== undefined) {
|
||||
p.occlusion.name = props.occlusion.name;
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, props.occlusion.name === 'on');
|
||||
if (props.occlusion.name === 'on') {
|
||||
p.occlusion.params = { ...props.occlusion.params };
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOcclusionKernelSize, props.occlusion.params.kernelSize);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOcclusionBias, props.occlusion.params.bias);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOcclusionRadius, props.occlusion.params.radius);
|
||||
}
|
||||
}
|
||||
|
||||
if (props.outline !== undefined) {
|
||||
p.outline.name = props.outline.name;
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, props.outline.name === 'on');
|
||||
if (props.outline.name === 'on') {
|
||||
p.outline.params = { ...props.outline.params };
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOutlineScale, props.outline.params.scale);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, props.outline.params.threshold);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.renderable.update();
|
||||
}
|
||||
|
||||
render(toDrawingBuffer: boolean) {
|
||||
ValueCell.update(this.renderable.values.uFar, this.camera.far);
|
||||
ValueCell.update(this.renderable.values.uNear, this.camera.near);
|
||||
ValueCell.update(this.renderable.values.uFogFar, this.camera.fogFar);
|
||||
ValueCell.update(this.renderable.values.uFogNear, this.camera.fogNear);
|
||||
ValueCell.update(this.renderable.values.dOrthographic, this.camera.state.mode === 'orthographic' ? 1 : 0);
|
||||
|
||||
const { gl, state } = this.webgl;
|
||||
if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
|
||||
} else {
|
||||
this.target.bind();
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
this.tmpTarget.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.fxaa.values.uTexSizeInv, Vec2.set(this.fxaa.values.uTexSizeInv.ref.value, 1 / width, 1 / height));
|
||||
}
|
||||
state.disable(gl.SCISSOR_TEST);
|
||||
}
|
||||
|
||||
private updateState(camera: ICamera) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
gl.viewport(x, y, width, height);
|
||||
gl.scissor(x, y, width, height);
|
||||
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
private _renderPostprocessing(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
const { values } = this.renderable;
|
||||
|
||||
ValueCell.updateIfChanged(values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(values.uFogFar, camera.fogFar);
|
||||
ValueCell.updateIfChanged(values.uFogNear, camera.fogNear);
|
||||
|
||||
let needsUpdate = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
if (values.dOrthographic.ref.value !== orthographic) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dOrthographic, orthographic);
|
||||
|
||||
const occlusion = props.occlusion.name === 'on';
|
||||
if (values.dOcclusionEnable.ref.value !== occlusion) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusion);
|
||||
if (props.occlusion.name === 'on') {
|
||||
const { kernelSize } = props.occlusion.params;
|
||||
if (values.dOcclusionKernelSize.ref.value !== kernelSize) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dOcclusionKernelSize, kernelSize);
|
||||
ValueCell.updateIfChanged(values.uOcclusionBias, props.occlusion.params.bias);
|
||||
ValueCell.updateIfChanged(values.uOcclusionRadius, props.occlusion.params.radius);
|
||||
}
|
||||
|
||||
const outline = props.outline.name === 'on';
|
||||
if (values.dOutlineEnable.ref.value !== outline) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dOutlineEnable, outline);
|
||||
if (props.outline.name === 'on') {
|
||||
ValueCell.updateIfChanged(values.uOutlineScale, props.outline.params.scale * this.webgl.pixelRatio);
|
||||
ValueCell.updateIfChanged(values.uOutlineThreshold, props.outline.params.threshold);
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
}
|
||||
|
||||
if (props.antialiasing.name === 'on') {
|
||||
this.tmpTarget.bind();
|
||||
} else if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.target.bind();
|
||||
}
|
||||
|
||||
this.updateState(camera);
|
||||
this.renderable.render();
|
||||
}
|
||||
}
|
||||
|
||||
private _renderFxaa(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
if (props.antialiasing.name === 'off') return;
|
||||
|
||||
const { values } = this.fxaa;
|
||||
|
||||
let needsUpdate = false;
|
||||
|
||||
const input = (props.occlusion.name === 'on' || props.outline.name === 'on')
|
||||
? this.tmpTarget.texture : this.drawPass.colorTarget.texture;
|
||||
if (values.tColor.ref.value !== input) {
|
||||
ValueCell.update(this.fxaa.values.tColor, input);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
const { edgeThresholdMin, edgeThresholdMax, iterations, subpixelQuality } = props.antialiasing.params;
|
||||
if (values.dEdgeThresholdMin.ref.value !== edgeThresholdMin) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dEdgeThresholdMin, edgeThresholdMin);
|
||||
if (values.dEdgeThresholdMax.ref.value !== edgeThresholdMax) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dEdgeThresholdMax, edgeThresholdMax);
|
||||
if (values.dIterations.ref.value !== iterations) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dIterations, iterations);
|
||||
if (values.dSubpixelQuality.ref.value !== subpixelQuality) needsUpdate = true;
|
||||
ValueCell.updateIfChanged(values.dSubpixelQuality, subpixelQuality);
|
||||
|
||||
if (needsUpdate) {
|
||||
this.fxaa.update();
|
||||
}
|
||||
|
||||
if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.target.bind();
|
||||
}
|
||||
|
||||
this.updateState(camera);
|
||||
this.fxaa.render();
|
||||
}
|
||||
|
||||
private _render(camera: ICamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
if (props.occlusion.name === 'on' || props.outline.name === 'on' || props.antialiasing.name === 'off') {
|
||||
this._renderPostprocessing(camera, toDrawingBuffer, props);
|
||||
}
|
||||
|
||||
if (props.antialiasing.name === 'on') {
|
||||
this._renderFxaa(camera, toDrawingBuffer, props);
|
||||
}
|
||||
}
|
||||
|
||||
render(camera: Camera | StereoCamera, toDrawingBuffer: boolean, props: PostprocessingProps) {
|
||||
if (StereoCamera.is(camera)) {
|
||||
this._render(camera.left, toDrawingBuffer, props);
|
||||
this._render(camera.right, toDrawingBuffer, props);
|
||||
} else {
|
||||
this._render(camera, toDrawingBuffer, props);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const FxaaSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSizeInv: UniformSpec('v2'),
|
||||
|
||||
dEdgeThresholdMin: DefineSpec('number'),
|
||||
dEdgeThresholdMax: DefineSpec('number'),
|
||||
dIterations: DefineSpec('number'),
|
||||
dSubpixelQuality: DefineSpec('number'),
|
||||
};
|
||||
const FxaaShaderCode = ShaderCode('fxaa', quad_vert, fxaa_frag);
|
||||
type FxaaRenderable = ComputeRenderable<Values<typeof FxaaSchema>>
|
||||
|
||||
function getFxaaRenderable(ctx: WebGLContext, colorTexture: Texture): FxaaRenderable {
|
||||
const width = colorTexture.getWidth();
|
||||
const height = colorTexture.getHeight();
|
||||
|
||||
const values: Values<typeof FxaaSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
|
||||
|
||||
dEdgeThresholdMin: ValueCell.create(0.0312),
|
||||
dEdgeThresholdMax: ValueCell.create(0.125),
|
||||
dIterations: ValueCell.create(12),
|
||||
dSubpixelQuality: ValueCell.create(0.75),
|
||||
};
|
||||
|
||||
const schema = { ...FxaaSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', FxaaShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
120
src/mol-canvas3d/passes/wboit.ts
Normal file
120
src/mol-canvas3d/passes/wboit.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import quad_vert from '../../mol-gl/shader/quad.vert';
|
||||
import evaluate_wboit_frag from '../../mol-gl/shader/evaluate-wboit.frag';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { isDebugMode } from '../../mol-util/debug';
|
||||
|
||||
const EvaluateWboitSchema = {
|
||||
...QuadSchema,
|
||||
tWboitA: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
tWboitB: TextureSpec('texture', 'rgba', 'float', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
};
|
||||
const EvaluateWboitShaderCode = ShaderCode('evaluate-wboit', quad_vert, evaluate_wboit_frag);
|
||||
type EvaluateWboitRenderable = ComputeRenderable<Values<typeof EvaluateWboitSchema>>
|
||||
|
||||
function getEvaluateWboitRenderable(ctx: WebGLContext, wboitATexture: Texture, wboitBTexture: Texture): EvaluateWboitRenderable {
|
||||
const values: Values<typeof EvaluateWboitSchema> = {
|
||||
...QuadValues,
|
||||
tWboitA: ValueCell.create(wboitATexture),
|
||||
tWboitB: ValueCell.create(wboitBTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(wboitATexture.getWidth(), wboitATexture.getHeight())),
|
||||
};
|
||||
|
||||
const schema = { ...EvaluateWboitSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', EvaluateWboitShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export class WboitPass {
|
||||
private readonly renderable: EvaluateWboitRenderable
|
||||
|
||||
private readonly framebuffer: Framebuffer
|
||||
private readonly textureA: Texture
|
||||
private readonly textureB: Texture
|
||||
|
||||
private _enabled = false;
|
||||
get enabled() {
|
||||
return this._enabled;
|
||||
}
|
||||
|
||||
bind() {
|
||||
const { state, gl } = this.webgl;
|
||||
|
||||
this.framebuffer.bind();
|
||||
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
|
||||
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ZERO, gl.ONE_MINUS_SRC_ALPHA);
|
||||
state.enable(gl.BLEND);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { state, gl } = this.webgl;
|
||||
|
||||
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
state.enable(gl.BLEND);
|
||||
|
||||
this.renderable.update();
|
||||
this.renderable.render();
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
this.textureA.define(width, height);
|
||||
this.textureB.define(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private webgl: WebGLContext, width: number, height: number) {
|
||||
const { resources, extensions } = webgl;
|
||||
const { drawBuffers, textureFloat, colorBufferFloat, depthTexture } = extensions;
|
||||
if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
|
||||
if (isDebugMode) console.log('Missing extensions required for "wboit"');
|
||||
return;
|
||||
}
|
||||
|
||||
this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.textureA.define(width, height);
|
||||
|
||||
this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.textureB.define(width, height);
|
||||
|
||||
this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
|
||||
|
||||
this.framebuffer = resources.framebuffer();
|
||||
this.framebuffer.bind();
|
||||
drawBuffers.drawBuffers([
|
||||
drawBuffers.COLOR_ATTACHMENT0,
|
||||
drawBuffers.COLOR_ATTACHMENT1,
|
||||
]);
|
||||
|
||||
this.textureA.attachFramebuffer(this.framebuffer, 'color0');
|
||||
this.textureB.attachFramebuffer(this.framebuffer, 'color1');
|
||||
|
||||
this._enabled = true;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
*/
|
||||
|
||||
/** Set canvas size taking `devicePixelRatio` into account */
|
||||
export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height: number) {
|
||||
canvas.width = Math.round(window.devicePixelRatio * width);
|
||||
canvas.height = Math.round(window.devicePixelRatio * height);
|
||||
export function setCanvasSize(canvas: HTMLCanvasElement, width: number, height: number, scale = 1) {
|
||||
canvas.width = Math.round(window.devicePixelRatio * scale * width);
|
||||
canvas.height = Math.round(window.devicePixelRatio * scale * height);
|
||||
Object.assign(canvas.style, { width: `${width}px`, height: `${height}px` });
|
||||
}
|
||||
|
||||
/** Resize canvas to container element taking `devicePixelRatio` into account */
|
||||
export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) {
|
||||
export function resizeCanvas (canvas: HTMLCanvasElement, container: Element, scale = 1) {
|
||||
let width = window.innerWidth;
|
||||
let height = window.innerHeight;
|
||||
if (container !== document.body) {
|
||||
@@ -20,7 +20,7 @@ export function resizeCanvas (canvas: HTMLCanvasElement, container: Element) {
|
||||
width = bounds.right - bounds.left;
|
||||
height = bounds.bottom - bounds.top;
|
||||
}
|
||||
setCanvasSize(canvas, width, height);
|
||||
setCanvasSize(canvas, width, height, scale);
|
||||
}
|
||||
|
||||
function _canvasToBlob(canvas: HTMLCanvasElement, callback: BlobCallback, type?: string, quality?: any) {
|
||||
|
||||
@@ -47,33 +47,40 @@ namespace ChunkedArray {
|
||||
export function add4<T>(array: ChunkedArray<T, 4>, x: T, y: T, z: T, w: T) {
|
||||
if (array.currentIndex >= array.currentSize) allocateNext(array);
|
||||
const c = array.currentChunk;
|
||||
c[array.currentIndex++] = x;
|
||||
c[array.currentIndex++] = y;
|
||||
c[array.currentIndex++] = z;
|
||||
c[array.currentIndex++] = w;
|
||||
const i = array.currentIndex;
|
||||
c[i] = x;
|
||||
c[i + 1] = y;
|
||||
c[i + 2] = z;
|
||||
c[i + 3] = w;
|
||||
array.currentIndex += 4;
|
||||
return array.elementCount++;
|
||||
}
|
||||
|
||||
export function add3<T>(array: ChunkedArray<T, 3>, x: T, y: T, z: T) {
|
||||
if (array.currentIndex >= array.currentSize) allocateNext(array);
|
||||
const c = array.currentChunk;
|
||||
c[array.currentIndex++] = x;
|
||||
c[array.currentIndex++] = y;
|
||||
c[array.currentIndex++] = z;
|
||||
const i = array.currentIndex;
|
||||
c[i] = x;
|
||||
c[i + 1] = y;
|
||||
c[i + 2] = z;
|
||||
array.currentIndex += 3;
|
||||
return array.elementCount++;
|
||||
}
|
||||
|
||||
export function add2<T>(array: ChunkedArray<T, 2>, x: T, y: T) {
|
||||
if (array.currentIndex >= array.currentSize) allocateNext(array);
|
||||
const c = array.currentChunk;
|
||||
c[array.currentIndex++] = x;
|
||||
c[array.currentIndex++] = y;
|
||||
const i = array.currentIndex;
|
||||
c[i] = x;
|
||||
c[i + 1] = y;
|
||||
array.currentIndex += 2;
|
||||
return array.elementCount++;
|
||||
}
|
||||
|
||||
export function add<T>(array: ChunkedArray<T, 1>, x: T) {
|
||||
if (array.currentIndex >= array.currentSize) allocateNext(array);
|
||||
array.currentChunk[array.currentIndex++] = x;
|
||||
array.currentChunk[array.currentIndex] = x;
|
||||
array.currentIndex += 1;
|
||||
return array.elementCount++;
|
||||
}
|
||||
|
||||
|
||||
@@ -46,11 +46,11 @@ export namespace BaseGeometry {
|
||||
hideIf: (params: PD.Values<Params>) => typeof params.quality !== 'undefined' && params.quality !== 'custom'
|
||||
};
|
||||
|
||||
export type Counts = { drawCount: number, groupCount: number, instanceCount: number }
|
||||
export type Counts = { drawCount: number, vertexCount: number, groupCount: number, instanceCount: number }
|
||||
|
||||
export function createSimple(colorValue = ColorNames.grey, sizeValue = 1, transform?: TransformData) {
|
||||
if (!transform) transform = createIdentityTransform();
|
||||
const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, () => NullLocation, false, () => false);
|
||||
const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
|
||||
const theme: Theme = {
|
||||
color: UniformColorTheme({}, { value: colorValue}),
|
||||
size: UniformSizeTheme({}, { value: sizeValue})
|
||||
@@ -62,6 +62,7 @@ export namespace BaseGeometry {
|
||||
return {
|
||||
alpha: ValueCell.create(props.alpha),
|
||||
uAlpha: ValueCell.create(props.alpha),
|
||||
uVertexCount: ValueCell.create(counts.vertexCount),
|
||||
uGroupCount: ValueCell.create(counts.groupCount),
|
||||
drawCount: ValueCell.create(counts.drawCount),
|
||||
};
|
||||
@@ -77,6 +78,7 @@ export namespace BaseGeometry {
|
||||
visible: true,
|
||||
alphaFactor: 1,
|
||||
pickable: true,
|
||||
colorOnly: false,
|
||||
opaque,
|
||||
writeDepth: opaque,
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ export function createClipping(count: number, clippingData?: ClippingData): Clip
|
||||
if (clippingData) {
|
||||
ValueCell.update(clippingData.tClipping, clipping);
|
||||
ValueCell.update(clippingData.uClippingTexDim, Vec2.create(clipping.width, clipping.height));
|
||||
ValueCell.update(clippingData.dClipping, count > 0);
|
||||
ValueCell.updateIfChanged(clippingData.dClipping, count > 0);
|
||||
return clippingData;
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ import { NullLocation } from '../../mol-model/location';
|
||||
import { LocationColor, ColorTheme } from '../../mol-theme/color';
|
||||
import { Geometry } from './geometry';
|
||||
|
||||
export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance'
|
||||
export type ColorType = 'uniform' | 'instance' | 'group' | 'groupInstance' | 'vertex' | 'vertexInstance'
|
||||
|
||||
export type ColorData = {
|
||||
uColor: ValueCell<Vec3>,
|
||||
@@ -22,21 +22,21 @@ export type ColorData = {
|
||||
dColorType: ValueCell<string>,
|
||||
}
|
||||
|
||||
export function createColors(locationIt: LocationIterator, colorTheme: ColorTheme<any>, colorData?: ColorData): ColorData {
|
||||
export function createColors(locationIt: LocationIterator, positionIt: LocationIterator, colorTheme: ColorTheme<any>, colorData?: ColorData): ColorData {
|
||||
switch (Geometry.getGranularity(locationIt, colorTheme.granularity)) {
|
||||
case 'uniform': return createUniformColor(locationIt, colorTheme.color, colorData);
|
||||
case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData);
|
||||
case 'group': return createGroupColor(locationIt, colorTheme.color, colorData);
|
||||
case 'groupInstance': return createGroupInstanceColor(locationIt, colorTheme.color, colorData);
|
||||
case 'instance': return createInstanceColor(locationIt, colorTheme.color, colorData);
|
||||
case 'vertex': return createVertexColor(positionIt, colorTheme.color, colorData);
|
||||
case 'vertexInstance': return createVertexInstanceColor(positionIt, colorTheme.color, colorData);
|
||||
}
|
||||
}
|
||||
|
||||
export function createValueColor(value: Color, colorData?: ColorData): ColorData {
|
||||
if (colorData) {
|
||||
ValueCell.update(colorData.uColor, Color.toVec3Normalized(colorData.uColor.ref.value, value));
|
||||
if (colorData.dColorType.ref.value !== 'uniform') {
|
||||
ValueCell.update(colorData.dColorType, 'uniform');
|
||||
}
|
||||
ValueCell.updateIfChanged(colorData.dColorType, 'uniform');
|
||||
return colorData;
|
||||
} else {
|
||||
return {
|
||||
@@ -57,13 +57,11 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
|
||||
if (colorData) {
|
||||
ValueCell.update(colorData.tColor, colors);
|
||||
ValueCell.update(colorData.uColorTexDim, Vec2.create(colors.width, colors.height));
|
||||
if (colorData.dColorType.ref.value !== type) {
|
||||
ValueCell.update(colorData.dColorType, type);
|
||||
}
|
||||
ValueCell.updateIfChanged(colorData.dColorType, type);
|
||||
return colorData;
|
||||
} else {
|
||||
return {
|
||||
uColor: ValueCell.create(Vec3.zero()),
|
||||
uColor: ValueCell.create(Vec3()),
|
||||
tColor: ValueCell.create(colors),
|
||||
uColorTexDim: ValueCell.create(Vec2.create(colors.width, colors.height)),
|
||||
dColorType: ValueCell.create(type),
|
||||
@@ -71,7 +69,7 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates color texture with color for each instance/unit */
|
||||
/** Creates color texture with color for each instance */
|
||||
export function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
|
||||
const { instanceCount } = locationIt;
|
||||
const colors = createTextureImage(Math.max(1, instanceCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
|
||||
@@ -84,7 +82,7 @@ export function createInstanceColor(locationIt: LocationIterator, color: Locatio
|
||||
return createTextureColor(colors, 'instance', colorData);
|
||||
}
|
||||
|
||||
/** Creates color texture with color for each group (i.e. shared across instances/units) */
|
||||
/** Creates color texture with color for each group (i.e. shared across instances) */
|
||||
export function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
|
||||
const { groupCount } = locationIt;
|
||||
const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
|
||||
@@ -96,7 +94,7 @@ export function createGroupColor(locationIt: LocationIterator, color: LocationCo
|
||||
return createTextureColor(colors, 'group', colorData);
|
||||
}
|
||||
|
||||
/** Creates color texture with color for each group in each instance (i.e. for each unit) */
|
||||
/** Creates color texture with color for each group in each instance */
|
||||
export function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
|
||||
const { groupCount, instanceCount } = locationIt;
|
||||
const count = instanceCount * groupCount;
|
||||
@@ -107,4 +105,36 @@ export function createGroupInstanceColor(locationIt: LocationIterator, color: Lo
|
||||
Color.toArray(color(location, isSecondary), colors.array, index * 3);
|
||||
}
|
||||
return createTextureColor(colors, 'groupInstance', colorData);
|
||||
}
|
||||
}
|
||||
|
||||
/** Creates color texture with color for each vertex (i.e. shared across instances) */
|
||||
export function createVertexColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
|
||||
const { groupCount, stride } = locationIt;
|
||||
const colors = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
|
||||
locationIt.reset();
|
||||
locationIt.voidInstances();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
const { location, isSecondary, groupIndex } = locationIt.move();
|
||||
const c = color(location, isSecondary);
|
||||
for (let i = 0; i < stride; ++i) {
|
||||
Color.toArray(c, colors.array, (groupIndex + i) * 3);
|
||||
}
|
||||
}
|
||||
return createTextureColor(colors, 'vertex', colorData);
|
||||
}
|
||||
|
||||
/** Creates color texture with color for each vertex in each instance */
|
||||
export function createVertexInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
|
||||
const { groupCount, instanceCount, stride } = locationIt;
|
||||
const count = instanceCount * groupCount;
|
||||
const colors = createTextureImage(Math.max(1, count), 3, Uint8Array, colorData && colorData.tColor.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext) {
|
||||
const { location, isSecondary, index } = locationIt.move();
|
||||
const c = color(location, isSecondary);
|
||||
for (let i = 0; i < stride; ++i) {
|
||||
Color.toArray(c, colors.array, (index + i) * 3);
|
||||
}
|
||||
}
|
||||
return createTextureColor(colors, 'vertexInstance', colorData);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { transformPositionArray } from '../../../mol-geo/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../../mol-geo/util/location-iterator';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { DirectVolumeValues } from '../../../mol-gl/renderable/direct-volume';
|
||||
import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
|
||||
import { calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
@@ -27,40 +26,50 @@ import { TransformData } from '../transform-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { createTransferFunctionTexture, getControlPointsFromVec2Array } from './transfer-function';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { Grid, Volume } from '../../../mol-model/volume';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
|
||||
const VolumeBox = Box();
|
||||
const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][];
|
||||
|
||||
export interface DirectVolume {
|
||||
readonly kind: 'direct-volume',
|
||||
|
||||
readonly gridTexture: ValueCell<Texture>,
|
||||
readonly gridTextureDim: ValueCell<Vec3>,
|
||||
readonly gridDimension: ValueCell<Vec3>,
|
||||
readonly gridTexture: ValueCell<Texture>
|
||||
readonly gridTextureDim: ValueCell<Vec3>
|
||||
readonly gridDimension: ValueCell<Vec3>
|
||||
readonly gridStats: ValueCell<Vec4> // [min, max, mean, sigma]
|
||||
readonly bboxSize: ValueCell<Vec3>
|
||||
readonly bboxMin: ValueCell<Vec3>
|
||||
readonly bboxMax: ValueCell<Vec3>
|
||||
readonly transform: ValueCell<Mat4>
|
||||
|
||||
readonly cellDim: ValueCell<Vec3>
|
||||
readonly unitToCartn: ValueCell<Mat4>
|
||||
readonly cartnToUnit: ValueCell<Mat4>
|
||||
readonly packedGroup: ValueCell<boolean>
|
||||
|
||||
/** Bounding sphere of the volume */
|
||||
boundingSphere: Sphere3D
|
||||
readonly boundingSphere: Sphere3D
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
}
|
||||
|
||||
export namespace DirectVolume {
|
||||
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, directVolume?: DirectVolume): DirectVolume {
|
||||
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, directVolume?: DirectVolume): DirectVolume {
|
||||
return directVolume ?
|
||||
update(bbox, gridDimension, transform, texture, directVolume) :
|
||||
fromData(bbox, gridDimension, transform, texture);
|
||||
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, directVolume) :
|
||||
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup);
|
||||
}
|
||||
|
||||
function hashCode(directVolume: DirectVolume) {
|
||||
return hashFnv32a([
|
||||
directVolume.bboxSize.ref.version, directVolume.gridDimension.ref.version,
|
||||
directVolume.gridTexture.ref.version, directVolume.transform.ref.version,
|
||||
directVolume.gridStats.ref.version
|
||||
]);
|
||||
}
|
||||
|
||||
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture): DirectVolume {
|
||||
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean): DirectVolume {
|
||||
const boundingSphere = Sphere3D();
|
||||
let currentHash = -1;
|
||||
|
||||
@@ -73,10 +82,14 @@ export namespace DirectVolume {
|
||||
gridDimension: ValueCell.create(gridDimension),
|
||||
gridTexture: ValueCell.create(texture),
|
||||
gridTextureDim: ValueCell.create(Vec3.create(width, height, depth)),
|
||||
gridStats: ValueCell.create(Vec4.create(stats.min, stats.max, stats.mean, stats.sigma)),
|
||||
bboxMin: ValueCell.create(bbox.min),
|
||||
bboxMax: ValueCell.create(bbox.max),
|
||||
bboxSize: ValueCell.create(Vec3.sub(Vec3.zero(), bbox.max, bbox.min)),
|
||||
bboxSize: ValueCell.create(Vec3.sub(Vec3(), bbox.max, bbox.min)),
|
||||
transform: ValueCell.create(transform),
|
||||
cellDim: ValueCell.create(cellDim),
|
||||
unitToCartn: ValueCell.create(unitToCartn),
|
||||
cartnToUnit: ValueCell.create(Mat4.invert(Mat4(), unitToCartn)),
|
||||
get boundingSphere() {
|
||||
const newHash = hashCode(directVolume);
|
||||
if (newHash !== currentHash) {
|
||||
@@ -86,11 +99,16 @@ export namespace DirectVolume {
|
||||
}
|
||||
return boundingSphere;
|
||||
},
|
||||
packedGroup: ValueCell.create(packedGroup),
|
||||
setBoundingSphere(sphere: Sphere3D) {
|
||||
Sphere3D.copy(boundingSphere, sphere);
|
||||
currentHash = hashCode(directVolume);
|
||||
}
|
||||
};
|
||||
return directVolume;
|
||||
}
|
||||
|
||||
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, texture: Texture, directVolume: DirectVolume): DirectVolume {
|
||||
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, directVolume: DirectVolume): DirectVolume {
|
||||
const width = texture.getWidth();
|
||||
const height = texture.getHeight();
|
||||
const depth = texture.getDepth();
|
||||
@@ -98,10 +116,15 @@ export namespace DirectVolume {
|
||||
ValueCell.update(directVolume.gridDimension, gridDimension);
|
||||
ValueCell.update(directVolume.gridTexture, texture);
|
||||
ValueCell.update(directVolume.gridTextureDim, Vec3.set(directVolume.gridTextureDim.ref.value, width, height, depth));
|
||||
ValueCell.update(directVolume.gridStats, Vec4.set(directVolume.gridStats.ref.value, stats.min, stats.max, stats.mean, stats.sigma));
|
||||
ValueCell.update(directVolume.bboxMin, bbox.min);
|
||||
ValueCell.update(directVolume.bboxMax, bbox.max);
|
||||
ValueCell.update(directVolume.bboxSize, Vec3.sub(directVolume.bboxSize.ref.value, bbox.max, bbox.min));
|
||||
ValueCell.update(directVolume.transform, transform);
|
||||
ValueCell.update(directVolume.cellDim, cellDim);
|
||||
ValueCell.update(directVolume.unitToCartn, unitToCartn);
|
||||
ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
|
||||
ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
|
||||
return directVolume;
|
||||
}
|
||||
|
||||
@@ -109,15 +132,45 @@ export namespace DirectVolume {
|
||||
return {} as DirectVolume; // TODO
|
||||
}
|
||||
|
||||
export function createRenderModeParam(stats?: Grid['stats']) {
|
||||
const isoValueParam = stats
|
||||
? Volume.createIsoValueParam(Volume.IsoValue.relative(2), stats)
|
||||
: Volume.IsoValueParam;
|
||||
|
||||
return PD.MappedStatic('volume', {
|
||||
isosurface: PD.Group({
|
||||
isoValue: isoValueParam,
|
||||
singleLayer: PD.Boolean(false, { isEssential: true }),
|
||||
}, { isFlat: true }),
|
||||
volume: PD.Group({
|
||||
controlPoints: PD.LineGraph([
|
||||
Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
|
||||
Vec2.create(0.79, 0.0), Vec2.create(0.8, 0.05), Vec2.create(0.85, 0.05), Vec2.create(0.86, 0.0),
|
||||
]),
|
||||
list: PD.ColorList({
|
||||
kind: 'interpolate',
|
||||
colors: [
|
||||
[ColorNames.white, 0],
|
||||
[ColorNames.red, 0.25],
|
||||
[ColorNames.white, 0.5],
|
||||
[ColorNames.blue, 0.75],
|
||||
[ColorNames.white, 1]
|
||||
]
|
||||
}, { offsets: true }),
|
||||
}, { isFlat: true })
|
||||
}, { isEssential: true });
|
||||
}
|
||||
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
isoValueNorm: PD.Numeric(0.22, { min: 0, max: 1, step: 0.01 }, { description: 'Normalized Isolevel Value' }),
|
||||
renderMode: PD.Select('volume', RenderModeOptions),
|
||||
controlPoints: PD.LineGraph([
|
||||
Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
|
||||
Vec2.create(0.79, 0.0), Vec2.create(0.8, 0.05), Vec2.create(0.85, 0.05), Vec2.create(0.86, 0.0),
|
||||
]),
|
||||
list: PD.ColorList('red-yellow-blue'),
|
||||
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
renderMode: createRenderModeParam(),
|
||||
stepsPerCell: PD.Numeric(5, { min: 1, max: 20, step: 1 }),
|
||||
jumpLength: PD.Numeric(0, { min: 0, max: 20, step: 0.1 }),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -129,29 +182,80 @@ export namespace DirectVolume {
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState,
|
||||
updateRenderableState
|
||||
updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(directVolume: DirectVolume, transform: TransformData): LocationIterator {
|
||||
const t = directVolume.transform.ref.value;
|
||||
const [x, y, z] = directVolume.gridDimension.ref.value;
|
||||
const groupCount = x * y * z;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
const k = Math.floor(groupIndex / z);
|
||||
p[0] = Math.floor(k / y);
|
||||
p[1] = k % y;
|
||||
p[2] = groupIndex % z;
|
||||
Vec3.transformMat4(p, p, t);
|
||||
if (instanceIndex >= 0) {
|
||||
Vec3.transformMat4Offset(p, p, m, 0, 0, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function getNormalizedIsoValue(out: Vec2, isoValue: Volume.IsoValue, stats: Vec4) {
|
||||
const [min, max, mean, sigma] = stats;
|
||||
const value = Volume.IsoValue.toAbsolute(isoValue, { min, max, mean, sigma }).absoluteValue;
|
||||
Vec2.set(out, (value - min) / (max - min), (0 - min) / (max - min));
|
||||
return out;
|
||||
}
|
||||
|
||||
function getMaxSteps(gridDim: Vec3, stepsPerCell: number) {
|
||||
return Math.ceil(Vec3.magnitude(gridDim) * stepsPerCell);
|
||||
}
|
||||
|
||||
function getStepScale(cellDim: Vec3, stepsPerCell: number) {
|
||||
return Math.min(...cellDim) * (1 / stepsPerCell);
|
||||
}
|
||||
|
||||
function getTransferScale(stepsPerCell: number) {
|
||||
return (1 / stepsPerCell);
|
||||
}
|
||||
|
||||
function createValues(directVolume: DirectVolume, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): DirectVolumeValues {
|
||||
const { gridTexture, gridTextureDim } = directVolume;
|
||||
const { gridTexture, gridTextureDim, gridStats } = directVolume;
|
||||
const { bboxSize, bboxMin, bboxMax, gridDimension, transform: gridTransform } = directVolume;
|
||||
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const positionIt = Utils.createPositionIterator(directVolume, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount };
|
||||
const [x, y, z] = gridDimension.ref.value;
|
||||
const counts = { drawCount: VolumeBox.indices.length, vertexCount: x * y * z, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(directVolume.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
|
||||
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);
|
||||
const transferTex = createTransferFunctionTexture(controlPoints, props.list.colors);
|
||||
const controlPoints = props.renderMode.name === 'volume' ? getControlPointsFromVec2Array(props.renderMode.params.controlPoints) : [];
|
||||
const transferTex = createTransferFunctionTexture(controlPoints, props.renderMode.name === 'volume' ? props.renderMode.params.list.colors : []);
|
||||
|
||||
const maxSteps = Math.ceil(Vec3.magnitude(gridDimension.ref.value)) * 2 * 5;
|
||||
const isoValue = props.renderMode.name === 'isosurface'
|
||||
? props.renderMode.params.isoValue
|
||||
: Volume.IsoValue.relative(2);
|
||||
|
||||
const singleLayer = props.renderMode.name === 'isosurface'
|
||||
? props.renderMode.params.singleLayer
|
||||
: false;
|
||||
|
||||
return {
|
||||
...color,
|
||||
@@ -168,19 +272,35 @@ export namespace DirectVolume {
|
||||
invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
|
||||
uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
|
||||
|
||||
uIsoValue: ValueCell.create(props.isoValueNorm),
|
||||
uIsoValue: ValueCell.create(getNormalizedIsoValue(Vec2(), isoValue, directVolume.gridStats.ref.value)),
|
||||
uBboxMin: bboxMin,
|
||||
uBboxMax: bboxMax,
|
||||
uBboxSize: bboxSize,
|
||||
dMaxSteps: ValueCell.create(maxSteps),
|
||||
uMaxSteps: ValueCell.create(getMaxSteps(gridDimension.ref.value, props.stepsPerCell)),
|
||||
uStepScale: ValueCell.create(getStepScale(directVolume.cellDim.ref.value, props.stepsPerCell)),
|
||||
uJumpLength: ValueCell.create(props.jumpLength),
|
||||
uTransform: gridTransform,
|
||||
uGridDim: gridDimension,
|
||||
dRenderMode: ValueCell.create(props.renderMode),
|
||||
dRenderMode: ValueCell.create(props.renderMode.name),
|
||||
tTransferTex: transferTex,
|
||||
uTransferScale: ValueCell.create(getTransferScale(props.stepsPerCell)),
|
||||
|
||||
dGridTexType: ValueCell.create(gridTexture.ref.value.getDepth() > 0 ? '3d' : '2d'),
|
||||
uGridTexDim: gridTextureDim,
|
||||
tGridTex: gridTexture,
|
||||
uGridStats: gridStats,
|
||||
|
||||
uCellDim: directVolume.cellDim,
|
||||
uCartnToUnit: directVolume.cartnToUnit,
|
||||
uUnitToCartn: directVolume.unitToCartn,
|
||||
dPackedGroup: directVolume.packedGroup,
|
||||
dSingleLayer: ValueCell.create(singleLayer),
|
||||
|
||||
dDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,12 +311,27 @@ export namespace DirectVolume {
|
||||
}
|
||||
|
||||
function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
|
||||
ValueCell.updateIfChanged(values.uIsoValue, props.isoValueNorm);
|
||||
ValueCell.updateIfChanged(values.alpha, props.alpha);
|
||||
ValueCell.updateIfChanged(values.uAlpha, props.alpha);
|
||||
ValueCell.updateIfChanged(values.dRenderMode, props.renderMode);
|
||||
ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
|
||||
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
|
||||
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
|
||||
ValueCell.updateIfChanged(values.dRenderMode, props.renderMode.name);
|
||||
|
||||
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);
|
||||
createTransferFunctionTexture(controlPoints, props.list.colors, values.tTransferTex);
|
||||
if (props.renderMode.name === 'isosurface') {
|
||||
ValueCell.updateIfChanged(values.uIsoValue, getNormalizedIsoValue(values.uIsoValue.ref.value, props.renderMode.params.isoValue, values.uGridStats.ref.value));
|
||||
ValueCell.updateIfChanged(values.dSingleLayer, props.renderMode.params.singleLayer);
|
||||
} else if (props.renderMode.name === 'volume') {
|
||||
const controlPoints = getControlPointsFromVec2Array(props.renderMode.params.controlPoints);
|
||||
createTransferFunctionTexture(controlPoints, props.renderMode.params.list.colors, values.tTransferTex);
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(values.uMaxSteps, getMaxSteps(values.uGridDim.ref.value, props.stepsPerCell));
|
||||
ValueCell.updateIfChanged(values.uStepScale, getStepScale(values.uCellDim.ref.value, props.stepsPerCell));
|
||||
ValueCell.updateIfChanged(values.uTransferScale, getTransferScale(props.stepsPerCell));
|
||||
ValueCell.updateIfChanged(values.uJumpLength, props.jumpLength);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: DirectVolumeValues, directVolume: DirectVolume) {
|
||||
@@ -215,27 +350,19 @@ export namespace DirectVolume {
|
||||
function createRenderableState(props: PD.Values<Params>): RenderableState {
|
||||
const state = BaseGeometry.createRenderableState(props);
|
||||
state.opaque = false;
|
||||
state.writeDepth = props.renderMode.name === 'isosurface';
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
|
||||
BaseGeometry.updateRenderableState(state, props);
|
||||
state.opaque = false;
|
||||
state.writeDepth = props.renderMode.name === 'isosurface';
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const mTmp = Mat4.identity();
|
||||
const mTmp2 = Mat4.identity();
|
||||
const vHalfUnit = Vec3.create(0.5, 0.5, 0.5);
|
||||
const tmpVertices = new Float32Array(VolumeBox.vertices.length);
|
||||
function getBoundingSphere(gridDimension: Vec3, gridTransform: Mat4) {
|
||||
tmpVertices.set(VolumeBox.vertices);
|
||||
Mat4.fromTranslation(mTmp, vHalfUnit);
|
||||
Mat4.mul(mTmp, Mat4.fromScaling(mTmp2, gridDimension), mTmp);
|
||||
Mat4.mul(mTmp, gridTransform, mTmp);
|
||||
transformPositionArray(mTmp, tmpVertices, 0, tmpVertices.length / 3);
|
||||
return calculateInvariantBoundingSphere(tmpVertices, tmpVertices.length / 3, 1);
|
||||
// return calculateBoundingSphere(tmpVertices, tmpVertices.length / 3, transform, transformCount)
|
||||
return Sphere3D.fromDimensionsAndTransform(Sphere3D(), gridDimension, gridTransform);
|
||||
}
|
||||
@@ -6,10 +6,11 @@
|
||||
|
||||
import { TextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { spline } from '../../../mol-math/interpolate';
|
||||
import { ColorScale, Color } from '../../../mol-util/color';
|
||||
import { ColorScale } from '../../../mol-util/color';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Vec2 } from '../../../mol-math/linear-algebra';
|
||||
import { ColorListName } from '../../../mol-util/color/lists';
|
||||
import { ColorListEntry } from '../../../mol-util/color/color';
|
||||
|
||||
export interface ControlPoint { x: number, alpha: number }
|
||||
|
||||
@@ -24,7 +25,7 @@ export function getControlPointsFromVec2Array(array: Vec2[]): ControlPoint[] {
|
||||
return array.map(v => ({ x: v[0], alpha: v[1] }));
|
||||
}
|
||||
|
||||
export function createTransferFunctionTexture(controlPoints: ControlPoint[], listOrName: Color[] | ColorListName, texture?: ValueCell<TextureImage<Uint8Array>>): ValueCell<TextureImage<Uint8Array>> {
|
||||
export function createTransferFunctionTexture(controlPoints: ControlPoint[], listOrName: ColorListEntry[] | ColorListName, texture?: ValueCell<TextureImage<Uint8Array>>): ValueCell<TextureImage<Uint8Array>> {
|
||||
const cp = [
|
||||
{ x: 0, alpha: 0 },
|
||||
{ x: 0, alpha: 0 },
|
||||
|
||||
@@ -54,6 +54,7 @@ export interface GeometryUtils<G extends Geometry, P extends PD.Params = Geometr
|
||||
updateBoundingSphere(values: V, geometry: G): void
|
||||
createRenderableState(props: Partial<PD.Values<P>>): RenderableState
|
||||
updateRenderableState(state: RenderableState, props: PD.Values<P>): void
|
||||
createPositionIterator(geometry: G, transform: TransformData): LocationIterator
|
||||
}
|
||||
|
||||
export namespace Geometry {
|
||||
@@ -72,6 +73,21 @@ export namespace Geometry {
|
||||
}
|
||||
}
|
||||
|
||||
export function getVertexCount(geometry: Geometry): number {
|
||||
switch (geometry.kind) {
|
||||
case 'mesh': return geometry.vertexCount;
|
||||
case 'points': return geometry.pointCount;
|
||||
case 'spheres': return geometry.sphereCount * 4;
|
||||
case 'text': return geometry.charCount * 4;
|
||||
case 'lines': return geometry.lineCount * 4;
|
||||
case 'direct-volume':
|
||||
const [x, y, z] = geometry.gridDimension.ref.value;
|
||||
return x * y * z;
|
||||
case 'image': return 4;
|
||||
case 'texture-mesh': return geometry.vertexCount / 3;
|
||||
}
|
||||
}
|
||||
|
||||
export function getGroupCount(geometry: Geometry): number {
|
||||
switch (geometry.kind) {
|
||||
case 'mesh':
|
||||
@@ -103,9 +119,7 @@ export namespace Geometry {
|
||||
}
|
||||
}
|
||||
|
||||
export function getGranularity(locationIt: LocationIterator, granularity: ColorType | SizeType) {
|
||||
// Always use 'group' granularity for 'complex' location iterators,
|
||||
// i.e. for which an instance may include multiple units
|
||||
return granularity === 'instance' && locationIt.isComplex ? 'group' : granularity;
|
||||
export function getGranularity<T extends ColorType | SizeType>(locationIt: LocationIterator, granularity: T) {
|
||||
return granularity === 'instance' && locationIt.nonInstanceable ? 'group' : granularity;
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere, TextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { calculateTransformBoundingSphere, TextureImage } from '../../../mol-gl/renderable/util';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
@@ -24,6 +24,8 @@ import { createEmptyTransparency } from '../transparency-data';
|
||||
import { ImageValues } from '../../../mol-gl/renderable/image';
|
||||
import { fillSerial } from '../../../mol-util/array';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
import { QuadPositions } from '../../../mol-gl/compute/util';
|
||||
|
||||
const QuadIndices = new Uint32Array([
|
||||
0, 1, 2,
|
||||
@@ -51,17 +53,17 @@ export { Image };
|
||||
interface Image {
|
||||
readonly kind: 'image',
|
||||
|
||||
readonly imageTexture: ValueCell<TextureImage<Float32Array>>,
|
||||
readonly imageTexture: ValueCell<TextureImage<Uint8Array>>,
|
||||
readonly imageTextureDim: ValueCell<Vec2>,
|
||||
readonly cornerBuffer: ValueCell<Float32Array>,
|
||||
readonly groupTexture: ValueCell<TextureImage<Float32Array>>,
|
||||
readonly groupTexture: ValueCell<TextureImage<Uint8Array>>,
|
||||
|
||||
/** Bounding sphere of the image */
|
||||
boundingSphere: Sphere3D
|
||||
}
|
||||
|
||||
namespace Image {
|
||||
export function create(imageTexture: TextureImage<Float32Array>, corners: Float32Array, groupTexture: TextureImage<Float32Array>, image?: Image): Image {
|
||||
export function create(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image?: Image): Image {
|
||||
return image ?
|
||||
update(imageTexture, corners, groupTexture, image) :
|
||||
fromData(imageTexture, corners, groupTexture);
|
||||
@@ -73,7 +75,7 @@ namespace Image {
|
||||
]);
|
||||
}
|
||||
|
||||
function fromData(imageTexture: TextureImage<Float32Array>, corners: Float32Array, groupTexture: TextureImage<Float32Array>): Image {
|
||||
function fromData(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>): Image {
|
||||
const boundingSphere = Sphere3D();
|
||||
let currentHash = -1;
|
||||
|
||||
@@ -99,7 +101,7 @@ namespace Image {
|
||||
return image;
|
||||
}
|
||||
|
||||
function update(imageTexture: TextureImage<Float32Array>, corners: Float32Array, groupTexture: TextureImage<Float32Array>, image: Image): Image {
|
||||
function update(imageTexture: TextureImage<Uint8Array>, corners: Float32Array, groupTexture: TextureImage<Uint8Array>, image: Image): Image {
|
||||
const width = imageTexture.width;
|
||||
const height = imageTexture.height;
|
||||
|
||||
@@ -128,19 +130,21 @@ namespace Image {
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState,
|
||||
updateRenderableState
|
||||
updateRenderableState,
|
||||
createPositionIterator: () => LocationIterator(1, 1, 1, () => NullLocation)
|
||||
};
|
||||
|
||||
function createValues(image: Image, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): ImageValues {
|
||||
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const positionIt = Utils.createPositionIterator(image, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: QuadIndices.length, groupCount, instanceCount };
|
||||
const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(image.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
@@ -211,5 +215,23 @@ namespace Image {
|
||||
//
|
||||
|
||||
function getBoundingSphere(corners: Float32Array) {
|
||||
return calculateInvariantBoundingSphere(corners, corners.length / 3, 1);
|
||||
const center = Vec3();
|
||||
const extrema: Vec3[] = [];
|
||||
for (let i = 0, il = corners.length; i < il; i += 3) {
|
||||
const e = Vec3.fromArray(Vec3(), corners, i);
|
||||
extrema.push(e);
|
||||
Vec3.add(center, center, e);
|
||||
}
|
||||
Vec3.scale(center, center, 1 / (corners.length / 3));
|
||||
|
||||
let radius = 0;
|
||||
for (const e of extrema) {
|
||||
const d = Vec3.distance(center, e);
|
||||
if (d > radius) radius = d;
|
||||
}
|
||||
|
||||
const sphere = Sphere3D.create(center, radius);
|
||||
Sphere3D.setExtrema(sphere, extrema);
|
||||
|
||||
return sphere;
|
||||
}
|
||||
@@ -21,27 +21,22 @@ const tmpVecA = Vec3();
|
||||
const tmpVecB = Vec3();
|
||||
const tmpDir = Vec3();
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd = ChunkedArray.add;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
|
||||
export namespace LinesBuilder {
|
||||
export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): LinesBuilder {
|
||||
const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, lines ? lines.mappingBuffer.ref.value : initialCount);
|
||||
const groups = ChunkedArray.create(Float32Array, 1, chunkSize, lines ? lines.groupBuffer.ref.value : initialCount);
|
||||
const indices = ChunkedArray.create(Uint32Array, 3, chunkSize * 3, lines ? lines.indexBuffer.ref.value : initialCount * 3);
|
||||
const starts = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.startBuffer.ref.value : initialCount);
|
||||
const ends = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.endBuffer.ref.value : initialCount);
|
||||
|
||||
const add = (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number) => {
|
||||
const offset = mappings.elementCount;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
ChunkedArray.add3(starts, startX, startY, startZ);
|
||||
ChunkedArray.add3(ends, endX, endY, endZ);
|
||||
ChunkedArray.add(groups, group);
|
||||
caAdd3(starts, startX, startY, startZ);
|
||||
caAdd3(ends, endX, endY, endZ);
|
||||
caAdd(groups, group);
|
||||
}
|
||||
ChunkedArray.add2(mappings, -1, 1);
|
||||
ChunkedArray.add2(mappings, -1, -1);
|
||||
ChunkedArray.add2(mappings, 1, 1);
|
||||
ChunkedArray.add2(mappings, 1, -1);
|
||||
ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
|
||||
ChunkedArray.add3(indices, offset + 1, offset + 3, offset + 2);
|
||||
};
|
||||
|
||||
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, group: number) => {
|
||||
@@ -78,13 +73,32 @@ export namespace LinesBuilder {
|
||||
}
|
||||
},
|
||||
getLines: () => {
|
||||
const mb = ChunkedArray.compact(mappings, true) as Float32Array;
|
||||
const ib = ChunkedArray.compact(indices, true) as Uint32Array;
|
||||
const lineCount = groups.elementCount / 4;
|
||||
const gb = ChunkedArray.compact(groups, true) as Float32Array;
|
||||
const sb = ChunkedArray.compact(starts, true) as Float32Array;
|
||||
const eb = ChunkedArray.compact(ends, true) as Float32Array;
|
||||
return Lines.create(mb, ib, gb, sb, eb, indices.elementCount / 2, lines);
|
||||
const mb = lines && lineCount <= lines.lineCount ? lines.mappingBuffer.ref.value : new Float32Array(lineCount * 8);
|
||||
const ib = lines && lineCount <= lines.lineCount ? lines.indexBuffer.ref.value : new Uint32Array(lineCount * 6);
|
||||
if (!lines || lineCount > lines.lineCount) fillMappingAndIndices(lineCount, mb, ib);
|
||||
return Lines.create(mb, ib, gb, sb, eb, lineCount, lines);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function fillMappingAndIndices(n: number, mb: Float32Array, ib: Uint32Array) {
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const mo = i * 8;
|
||||
mb[mo] = -1; mb[mo + 1] = -1;
|
||||
mb[mo + 2] = 1; mb[mo + 3] = -1;
|
||||
mb[mo + 4] = -1; mb[mo + 5] = 1;
|
||||
mb[mo + 6] = 1; mb[mo + 7] = 1;
|
||||
}
|
||||
|
||||
for (let i = 0; i < n; ++i) {
|
||||
const o = i * 4;
|
||||
const io = i * 6;
|
||||
ib[io] = o; ib[io + 1] = o + 1; ib[io + 2] = o + 2;
|
||||
ib[io + 3] = o + 1; ib[io + 4] = o + 3; ib[io + 5] = o + 2;
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,14 @@
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Mat4, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createColors } from '../color-data';
|
||||
import { createMarkers } from '../marker-data';
|
||||
import { createSizes } from '../size-data';
|
||||
import { TransformData } from '../transform-data';
|
||||
import { LocationIterator } from '../../util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../util/location-iterator';
|
||||
import { LinesValues } from '../../../mol-gl/renderable/lines';
|
||||
import { Mesh } from '../mesh/mesh';
|
||||
import { LinesBuilder } from './lines-builder';
|
||||
@@ -140,9 +140,11 @@ export namespace Lines {
|
||||
}
|
||||
|
||||
function update(mappings: Float32Array, indices: Uint32Array, groups: Float32Array, starts: Float32Array, ends: Float32Array, lineCount: number, lines: Lines) {
|
||||
if (lineCount > lines.lineCount) {
|
||||
ValueCell.update(lines.mappingBuffer, mappings);
|
||||
ValueCell.update(lines.indexBuffer, indices);
|
||||
}
|
||||
lines.lineCount = lineCount;
|
||||
ValueCell.update(lines.mappingBuffer, mappings);
|
||||
ValueCell.update(lines.indexBuffer, indices);
|
||||
ValueCell.update(lines.groupBuffer, groups);
|
||||
ValueCell.update(lines.startBuffer, starts);
|
||||
ValueCell.update(lines.endBuffer, ends);
|
||||
@@ -175,19 +177,42 @@ export namespace Lines {
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState: BaseGeometry.createRenderableState,
|
||||
updateRenderableState: BaseGeometry.updateRenderableState
|
||||
updateRenderableState: BaseGeometry.updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(lines: Lines, transform: TransformData): LocationIterator {
|
||||
const groupCount = lines.lineCount * 4;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const s = lines.startBuffer.ref.value;
|
||||
const e = lines.endBuffer.ref.value;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
const v = groupIndex % 4 === 0 ? s : e;
|
||||
if (instanceIndex < 0) {
|
||||
Vec3.fromArray(p, v, groupIndex * 3);
|
||||
} else {
|
||||
Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 2, getLocation);
|
||||
}
|
||||
|
||||
function createValues(lines: Lines, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): LinesValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const positionIt = createPositionIterator(lines, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount };
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.lineCount * 4, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(lines.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,19 +7,23 @@
|
||||
import { Vec3, Mat4 } from '../../../../mol-math/linear-algebra';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
import { Primitive, transformPrimitive } from '../../../primitive/primitive';
|
||||
import { Cylinder, CylinderProps } from '../../../primitive/cylinder';
|
||||
import { Cylinder, CylinderProps, DefaultCylinderProps } from '../../../primitive/cylinder';
|
||||
import { Prism } from '../../../primitive/prism';
|
||||
import { polygon } from '../../../primitive/polygon';
|
||||
import { hashFnv32a } from '../../../../mol-data/util';
|
||||
|
||||
const cylinderMap = new Map<string, Primitive>();
|
||||
const cylinderMap = new Map<number, Primitive>();
|
||||
const up = Vec3.create(0, 1, 0);
|
||||
|
||||
const tmpCylinderDir = Vec3.zero();
|
||||
const tmpCylinderMatDir = Vec3.zero();
|
||||
const tmpCylinderCenter = Vec3.zero();
|
||||
const tmpCylinderMat = Mat4.zero();
|
||||
const tmpCylinderStart = Vec3.zero();
|
||||
const tmpUp = Vec3.zero();
|
||||
const tmpCylinderDir = Vec3();
|
||||
const tmpCylinderMatDir = Vec3();
|
||||
const tmpCylinderCenter = Vec3();
|
||||
const tmpCylinderMat = Mat4();
|
||||
const tmpCylinderMatRot = Mat4();
|
||||
const tmpCylinderScale = Vec3();
|
||||
const tmpCylinderMatScale = Mat4();
|
||||
const tmpCylinderStart = Vec3();
|
||||
const tmpUp = Vec3();
|
||||
|
||||
function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number) {
|
||||
Vec3.setMagnitude(tmpCylinderMatDir, dir, length / 2);
|
||||
@@ -27,17 +31,34 @@ function setCylinderMat(m: Mat4, start: Vec3, dir: Vec3, length: number) {
|
||||
// ensure the direction used to create the rotation is always pointing in the same
|
||||
// direction so the triangles of adjacent cylinder will line up
|
||||
Vec3.matchDirection(tmpUp, up, tmpCylinderMatDir);
|
||||
Vec3.makeRotation(m, tmpUp, tmpCylinderMatDir);
|
||||
Mat4.fromScaling(tmpCylinderMatScale, Vec3.set(tmpCylinderScale, 1, length, 1));
|
||||
Vec3.makeRotation(tmpCylinderMatRot, tmpUp, tmpCylinderMatDir);
|
||||
Mat4.mul(m, tmpCylinderMatRot, tmpCylinderMatScale);
|
||||
return Mat4.setTranslation(m, tmpCylinderCenter);
|
||||
}
|
||||
|
||||
const tmpPropValues = new Int32Array(9);
|
||||
function getCylinderPropsKey(props: CylinderProps) {
|
||||
tmpPropValues[0] = Math.round(1000 * (props.radiusTop ?? DefaultCylinderProps.radiusTop));
|
||||
tmpPropValues[1] = Math.round(1000 * (props.radiusBottom ?? DefaultCylinderProps.radiusBottom));
|
||||
tmpPropValues[2] = Math.round(1000 * (props.height ?? DefaultCylinderProps.height));
|
||||
tmpPropValues[3] = props.radialSegments ?? DefaultCylinderProps.radialSegments;
|
||||
tmpPropValues[4] = props.heightSegments ?? DefaultCylinderProps.heightSegments;
|
||||
tmpPropValues[5] = (props.topCap ?? DefaultCylinderProps.topCap) ? 1 : 0;
|
||||
tmpPropValues[6] = (props.bottomCap ?? DefaultCylinderProps.bottomCap) ? 1 : 0;
|
||||
tmpPropValues[7] = Math.round(1000 * (props.thetaStart ?? DefaultCylinderProps.thetaStart));
|
||||
tmpPropValues[8] = Math.round(1000 * (props.thetaLength ?? DefaultCylinderProps.thetaLength));
|
||||
return hashFnv32a(tmpPropValues);
|
||||
}
|
||||
|
||||
function getCylinder(props: CylinderProps) {
|
||||
const key = JSON.stringify(props);
|
||||
const key = getCylinderPropsKey(props);
|
||||
let cylinder = cylinderMap.get(key);
|
||||
if (cylinder === undefined) {
|
||||
if (props.radialSegments && props.radialSegments <= 4) {
|
||||
const box = Prism(polygon(4, true, props.radiusTop), props);
|
||||
cylinder = transformPrimitive(box, Mat4.rotX90);
|
||||
const sideCount = Math.max(3, props.radialSegments);
|
||||
const prism = Prism(polygon(sideCount, true, props.radiusTop), props);
|
||||
cylinder = transformPrimitive(prism, Mat4.rotX90);
|
||||
} else {
|
||||
cylinder = Cylinder(props);
|
||||
}
|
||||
@@ -46,17 +67,17 @@ function getCylinder(props: CylinderProps) {
|
||||
return cylinder;
|
||||
}
|
||||
|
||||
export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: CylinderProps) {
|
||||
export type BasicCylinderProps = Omit<CylinderProps, 'height'>
|
||||
|
||||
export function addCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
props.height = d;
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
setCylinderMat(tmpCylinderMat, start, tmpCylinderDir, d);
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, getCylinder(props));
|
||||
}
|
||||
|
||||
export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: CylinderProps) {
|
||||
export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, shift: Vec3, props: BasicCylinderProps) {
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
props.height = d;
|
||||
const cylinder = getCylinder(props);
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
// positivly shifted cylinder
|
||||
@@ -69,7 +90,7 @@ export function addDoubleCylinder(state: MeshBuilder.State, start: Vec3, end: Ve
|
||||
MeshBuilder.addPrimitive(state, tmpCylinderMat, cylinder);
|
||||
}
|
||||
|
||||
export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: CylinderProps) {
|
||||
export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec3, end: Vec3, lengthScale: number, segmentCount: number, props: BasicCylinderProps) {
|
||||
const s = Math.floor(segmentCount / 2);
|
||||
const step = 1 / segmentCount;
|
||||
|
||||
@@ -81,7 +102,6 @@ export function addFixedCountDashedCylinder(state: MeshBuilder.State, start: Vec
|
||||
}
|
||||
|
||||
const d = Vec3.distance(start, end) * lengthScale;
|
||||
props.height = d * step;
|
||||
const cylinder = getCylinder(props);
|
||||
Vec3.sub(tmpCylinderDir, end, start);
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -9,16 +9,28 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
import { ChunkedArray } from '../../../../mol-data/util';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
|
||||
const tA = Vec3.zero();
|
||||
const tB = Vec3.zero();
|
||||
const tV = Vec3.zero();
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3magnitude = Vec3.magnitude;
|
||||
const v3sub = Vec3.sub;
|
||||
const v3add = Vec3.add;
|
||||
const v3scale = Vec3.scale;
|
||||
const v3negate = Vec3.negate;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3cross = Vec3.cross;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
const horizontalVector = Vec3.zero();
|
||||
const verticalVector = Vec3.zero();
|
||||
const normalOffset = Vec3.zero();
|
||||
const positionVector = Vec3.zero();
|
||||
const normalVector = Vec3.zero();
|
||||
const torsionVector = Vec3.zero();
|
||||
const tA = Vec3();
|
||||
const tB = Vec3();
|
||||
const tV = Vec3();
|
||||
|
||||
const horizontalVector = Vec3();
|
||||
const verticalVector = Vec3();
|
||||
const normalOffset = Vec3();
|
||||
const positionVector = Vec3();
|
||||
const normalVector = Vec3();
|
||||
const torsionVector = Vec3();
|
||||
|
||||
/** set arrowHeight = 0 for no arrow */
|
||||
export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, arrowHeight: number) {
|
||||
@@ -28,9 +40,9 @@ export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<num
|
||||
let offsetLength = 0;
|
||||
|
||||
if (arrowHeight > 0) {
|
||||
Vec3.fromArray(tA, controlPoints, 0);
|
||||
Vec3.fromArray(tB, controlPoints, linearSegments * 3);
|
||||
offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA));
|
||||
v3fromArray(tA, controlPoints, 0);
|
||||
v3fromArray(tB, controlPoints, linearSegments * 3);
|
||||
offsetLength = arrowHeight / v3magnitude(v3sub(tV, tB, tA));
|
||||
}
|
||||
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
@@ -40,62 +52,62 @@ export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<num
|
||||
const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
|
||||
const i3 = i * 3;
|
||||
|
||||
Vec3.fromArray(verticalVector, normalVectors, i3);
|
||||
Vec3.scale(verticalVector, verticalVector, actualHeight);
|
||||
v3fromArray(verticalVector, normalVectors, i3);
|
||||
v3scale(verticalVector, verticalVector, actualHeight);
|
||||
|
||||
Vec3.fromArray(horizontalVector, binormalVectors, i3);
|
||||
Vec3.scale(horizontalVector, horizontalVector, width);
|
||||
v3fromArray(horizontalVector, binormalVectors, i3);
|
||||
v3scale(horizontalVector, horizontalVector, width);
|
||||
|
||||
if (arrowHeight > 0) {
|
||||
Vec3.fromArray(tA, normalVectors, i3);
|
||||
Vec3.fromArray(tB, binormalVectors, i3);
|
||||
Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength);
|
||||
v3fromArray(tA, normalVectors, i3);
|
||||
v3fromArray(tB, binormalVectors, i3);
|
||||
v3scale(normalOffset, v3cross(normalOffset, tA, tB), offsetLength);
|
||||
}
|
||||
|
||||
Vec3.fromArray(positionVector, controlPoints, i3);
|
||||
Vec3.fromArray(normalVector, normalVectors, i3);
|
||||
Vec3.fromArray(torsionVector, binormalVectors, i3);
|
||||
v3fromArray(positionVector, controlPoints, i3);
|
||||
v3fromArray(normalVector, normalVectors, i3);
|
||||
v3fromArray(torsionVector, binormalVectors, i3);
|
||||
|
||||
Vec3.add(tA, positionVector, verticalVector);
|
||||
Vec3.negate(tB, torsionVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3add(tA, positionVector, verticalVector);
|
||||
v3negate(tB, torsionVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.sub(tA, positionVector, verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3sub(tA, positionVector, verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.add(tA, positionVector, verticalVector);
|
||||
Vec3.copy(tB, torsionVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3add(tA, positionVector, verticalVector);
|
||||
v3copy(tB, torsionVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.sub(tA, positionVector, verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3sub(tA, positionVector, verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < linearSegments; ++i) {
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 4,
|
||||
vertexCount + (i + 1) * 4 + 1,
|
||||
vertexCount + i * 4 + 1
|
||||
);
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 4,
|
||||
vertexCount + (i + 1) * 4,
|
||||
vertexCount + (i + 1) * 4 + 1
|
||||
);
|
||||
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 4 + 2 + 1,
|
||||
vertexCount + (i + 1) * 4 + 2 + 1,
|
||||
vertexCount + i * 4 + 2
|
||||
);
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 4 + 2,
|
||||
vertexCount + (i + 1) * 4 + 2 + 1,
|
||||
@@ -104,5 +116,5 @@ export function addRibbon(state: MeshBuilder.State, controlPoints: ArrayLike<num
|
||||
}
|
||||
|
||||
const addedVertexCount = (linearSegments + 1) * 4;
|
||||
for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup);
|
||||
for (let i = 0, il = addedVertexCount; i < il; ++i) caAdd(groups, currentGroup);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -9,65 +9,77 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
import { ChunkedArray } from '../../../../mol-data/util';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
|
||||
const tA = Vec3.zero();
|
||||
const tB = Vec3.zero();
|
||||
const tV = Vec3.zero();
|
||||
const tA = Vec3();
|
||||
const tB = Vec3();
|
||||
const tV = Vec3();
|
||||
|
||||
const horizontalVector = Vec3.zero();
|
||||
const verticalVector = Vec3.zero();
|
||||
const verticalRightVector = Vec3.zero();
|
||||
const verticalLeftVector = Vec3.zero();
|
||||
const normalOffset = Vec3.zero();
|
||||
const positionVector = Vec3.zero();
|
||||
const normalVector = Vec3.zero();
|
||||
const torsionVector = Vec3.zero();
|
||||
const horizontalVector = Vec3();
|
||||
const verticalVector = Vec3();
|
||||
const verticalRightVector = Vec3();
|
||||
const verticalLeftVector = Vec3();
|
||||
const normalOffset = Vec3();
|
||||
const positionVector = Vec3();
|
||||
const normalVector = Vec3();
|
||||
const torsionVector = Vec3();
|
||||
|
||||
const p1 = Vec3.zero();
|
||||
const p2 = Vec3.zero();
|
||||
const p3 = Vec3.zero();
|
||||
const p4 = Vec3.zero();
|
||||
const p1 = Vec3();
|
||||
const p2 = Vec3();
|
||||
const p3 = Vec3();
|
||||
const p4 = Vec3();
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3scale = Vec3.scale;
|
||||
const v3add = Vec3.add;
|
||||
const v3sub = Vec3.sub;
|
||||
const v3magnitude = Vec3.magnitude;
|
||||
const v3negate = Vec3.negate;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3cross = Vec3.cross;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, width: number, leftHeight: number, rightHeight: number) {
|
||||
const { vertices, normals, indices } = state;
|
||||
const vertexCount = vertices.elementCount;
|
||||
|
||||
Vec3.fromArray(verticalLeftVector, normalVectors, offset);
|
||||
Vec3.scale(verticalLeftVector, verticalLeftVector, leftHeight);
|
||||
v3fromArray(verticalLeftVector, normalVectors, offset);
|
||||
v3scale(verticalLeftVector, verticalLeftVector, leftHeight);
|
||||
|
||||
Vec3.fromArray(verticalRightVector, normalVectors, offset);
|
||||
Vec3.scale(verticalRightVector, verticalRightVector, rightHeight);
|
||||
v3fromArray(verticalRightVector, normalVectors, offset);
|
||||
v3scale(verticalRightVector, verticalRightVector, rightHeight);
|
||||
|
||||
Vec3.fromArray(horizontalVector, binormalVectors, offset);
|
||||
Vec3.scale(horizontalVector, horizontalVector, width);
|
||||
v3fromArray(horizontalVector, binormalVectors, offset);
|
||||
v3scale(horizontalVector, horizontalVector, width);
|
||||
|
||||
Vec3.fromArray(positionVector, controlPoints, offset);
|
||||
v3fromArray(positionVector, controlPoints, offset);
|
||||
|
||||
Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalRightVector);
|
||||
Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalLeftVector);
|
||||
Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalLeftVector);
|
||||
Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalRightVector);
|
||||
v3add(p1, v3add(p1, positionVector, horizontalVector), verticalRightVector);
|
||||
v3sub(p2, v3add(p2, positionVector, horizontalVector), verticalLeftVector);
|
||||
v3sub(p3, v3sub(p3, positionVector, horizontalVector), verticalLeftVector);
|
||||
v3add(p4, v3sub(p4, positionVector, horizontalVector), verticalRightVector);
|
||||
|
||||
if (leftHeight < rightHeight) {
|
||||
ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]);
|
||||
ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]);
|
||||
ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]);
|
||||
ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]);
|
||||
Vec3.copy(verticalVector, verticalRightVector);
|
||||
caAdd3(vertices, p4[0], p4[1], p4[2]);
|
||||
caAdd3(vertices, p3[0], p3[1], p3[2]);
|
||||
caAdd3(vertices, p2[0], p2[1], p2[2]);
|
||||
caAdd3(vertices, p1[0], p1[1], p1[2]);
|
||||
v3copy(verticalVector, verticalRightVector);
|
||||
} else {
|
||||
ChunkedArray.add3(vertices, p1[0], p1[1], p1[2]);
|
||||
ChunkedArray.add3(vertices, p2[0], p2[1], p2[2]);
|
||||
ChunkedArray.add3(vertices, p3[0], p3[1], p3[2]);
|
||||
ChunkedArray.add3(vertices, p4[0], p4[1], p4[2]);
|
||||
Vec3.copy(verticalVector, verticalLeftVector);
|
||||
caAdd3(vertices, p1[0], p1[1], p1[2]);
|
||||
caAdd3(vertices, p2[0], p2[1], p2[2]);
|
||||
caAdd3(vertices, p3[0], p3[1], p3[2]);
|
||||
caAdd3(vertices, p4[0], p4[1], p4[2]);
|
||||
v3copy(verticalVector, verticalLeftVector);
|
||||
}
|
||||
|
||||
Vec3.cross(normalVector, horizontalVector, verticalVector);
|
||||
v3cross(normalVector, horizontalVector, verticalVector);
|
||||
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
}
|
||||
ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
|
||||
ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
|
||||
caAdd3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
|
||||
caAdd3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
|
||||
}
|
||||
|
||||
/** set arrowHeight = 0 for no arrow */
|
||||
@@ -78,9 +90,9 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
let offsetLength = 0;
|
||||
|
||||
if (arrowHeight > 0) {
|
||||
Vec3.fromArray(tA, controlPoints, 0);
|
||||
Vec3.fromArray(tB, controlPoints, linearSegments * 3);
|
||||
offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA));
|
||||
v3fromArray(tA, controlPoints, 0);
|
||||
v3fromArray(tB, controlPoints, linearSegments * 3);
|
||||
offsetLength = arrowHeight / v3magnitude(v3sub(tV, tB, tA));
|
||||
}
|
||||
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
@@ -90,70 +102,70 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
|
||||
const i3 = i * 3;
|
||||
|
||||
Vec3.fromArray(verticalVector, normalVectors, i3);
|
||||
Vec3.scale(verticalVector, verticalVector, actualHeight);
|
||||
v3fromArray(verticalVector, normalVectors, i3);
|
||||
v3scale(verticalVector, verticalVector, actualHeight);
|
||||
|
||||
Vec3.fromArray(horizontalVector, binormalVectors, i3);
|
||||
Vec3.scale(horizontalVector, horizontalVector, width);
|
||||
v3fromArray(horizontalVector, binormalVectors, i3);
|
||||
v3scale(horizontalVector, horizontalVector, width);
|
||||
|
||||
if (arrowHeight > 0) {
|
||||
Vec3.fromArray(tA, normalVectors, i3);
|
||||
Vec3.fromArray(tB, binormalVectors, i3);
|
||||
Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength);
|
||||
v3fromArray(tA, normalVectors, i3);
|
||||
v3fromArray(tB, binormalVectors, i3);
|
||||
v3scale(normalOffset, v3cross(normalOffset, tA, tB), offsetLength);
|
||||
}
|
||||
|
||||
Vec3.fromArray(positionVector, controlPoints, i3);
|
||||
Vec3.fromArray(normalVector, normalVectors, i3);
|
||||
Vec3.fromArray(torsionVector, binormalVectors, i3);
|
||||
v3fromArray(positionVector, controlPoints, i3);
|
||||
v3fromArray(normalVector, normalVectors, i3);
|
||||
v3fromArray(torsionVector, binormalVectors, i3);
|
||||
|
||||
Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
|
||||
Vec3.copy(tB, normalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3add(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
|
||||
v3copy(tB, normalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3add(tA, v3sub(tA, positionVector, horizontalVector), verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
Vec3.add(tB, Vec3.negate(tB, torsionVector), normalOffset);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
// v3add(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3add(tB, v3negate(tB, torsionVector), normalOffset);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3sub(tA, v3sub(tA, positionVector, horizontalVector), verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
Vec3.negate(tB, normalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
// v3sub(tA, v3sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3negate(tB, normalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3sub(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
// Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
Vec3.add(tB, torsionVector, normalOffset);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
// v3sub(tA, v3add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
|
||||
v3add(tB, torsionVector, normalOffset);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
|
||||
Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector);
|
||||
ChunkedArray.add3(vertices, tA[0], tA[1], tA[2]);
|
||||
ChunkedArray.add3(normals, tB[0], tB[1], tB[2]);
|
||||
v3add(tA, v3add(tA, positionVector, horizontalVector), verticalVector);
|
||||
caAdd3(vertices, tA[0], tA[1], tA[2]);
|
||||
caAdd3(normals, tB[0], tB[1], tB[2]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < linearSegments; ++i) {
|
||||
// the triangles are arranged such that opposing triangles of the sheet align
|
||||
// which prevents triangle intersection within tight curves
|
||||
for (let j = 0; j < 2; j++) {
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 8 + 2 * j, // a
|
||||
vertexCount + (i + 1) * 8 + 2 * j + 1, // c
|
||||
vertexCount + i * 8 + 2 * j + 1 // b
|
||||
);
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 8 + 2 * j, // a
|
||||
vertexCount + (i + 1) * 8 + 2 * j, // d
|
||||
@@ -161,13 +173,13 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
);
|
||||
}
|
||||
for (let j = 2; j < 4; j++) {
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * 8 + 2 * j, // a
|
||||
vertexCount + (i + 1) * 8 + 2 * j, // d
|
||||
vertexCount + i * 8 + 2 * j + 1, // b
|
||||
);
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + (i + 1) * 8 + 2 * j, // d
|
||||
vertexCount + (i + 1) * 8 + 2 * j + 1, // c
|
||||
@@ -198,5 +210,5 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
|
||||
const addedVertexCount = (linearSegments + 1) * 8 +
|
||||
(startCap ? 4 : (arrowHeight > 0 ? 8 : 0)) +
|
||||
(endCap && arrowHeight === 0 ? 4 : 0);
|
||||
for (let i = 0, il = addedVertexCount; i < il; ++i) ChunkedArray.add(groups, currentGroup);
|
||||
for (let i = 0, il = addedVertexCount; i < il; ++i) caAdd(groups, currentGroup);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -9,11 +9,11 @@ import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
import { ChunkedArray } from '../../../../mol-data/util';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
|
||||
const normalVector = Vec3.zero();
|
||||
const surfacePoint = Vec3.zero();
|
||||
const controlPoint = Vec3.zero();
|
||||
const u = Vec3.zero();
|
||||
const v = Vec3.zero();
|
||||
const normalVector = Vec3();
|
||||
const surfacePoint = Vec3();
|
||||
const controlPoint = Vec3();
|
||||
const u = Vec3();
|
||||
const v = Vec3();
|
||||
|
||||
function add2AndScale2(out: Vec3, a: Vec3, b: Vec3, sa: number, sb: number) {
|
||||
out[0] = (a[0] * sa) + (b[0] * sb);
|
||||
@@ -27,53 +27,70 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
|
||||
out[2] = (a[2] * sa) + (b[2] * sb) + c[2];
|
||||
}
|
||||
|
||||
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, waveFactor: number, startCap: boolean, endCap: boolean) {
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3normalize = Vec3.normalize;
|
||||
const v3negate = Vec3.negate;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3cross = Vec3.cross;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
|
||||
const CosSinCache = new Map<number, { cos: number[], sin: number[] }>();
|
||||
function getCosSin(radialSegments: number) {
|
||||
if (!CosSinCache.has(radialSegments)) {
|
||||
const cos: number[] = [];
|
||||
const sin: number[] = [];
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
const t = 2 * Math.PI * j / radialSegments;
|
||||
cos[j] = Math.cos(t);
|
||||
sin[j] = Math.sin(t);
|
||||
}
|
||||
CosSinCache.set(radialSegments, { cos, sin });
|
||||
}
|
||||
return CosSinCache.get(radialSegments)!;
|
||||
}
|
||||
|
||||
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean) {
|
||||
const { currentGroup, vertices, normals, indices, groups } = state;
|
||||
|
||||
let vertexCount = vertices.elementCount;
|
||||
const di = 1 / linearSegments;
|
||||
|
||||
const { cos, sin } = getCosSin(radialSegments);
|
||||
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
const i3 = i * 3;
|
||||
Vec3.fromArray(u, normalVectors, i3);
|
||||
Vec3.fromArray(v, binormalVectors, i3);
|
||||
Vec3.fromArray(controlPoint, controlPoints, i3);
|
||||
v3fromArray(u, normalVectors, i3);
|
||||
v3fromArray(v, binormalVectors, i3);
|
||||
v3fromArray(controlPoint, controlPoints, i3);
|
||||
|
||||
const width = widthValues[i];
|
||||
const height = heightValues[i];
|
||||
|
||||
const tt = di * i - 0.5;
|
||||
const ff = 1 + (waveFactor - 1) * (Math.cos(2 * Math.PI * tt) + 1);
|
||||
const w = ff * width, h = ff * height;
|
||||
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
const t = 2 * Math.PI * j / radialSegments;
|
||||
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, h * Math.cos(t), w * Math.sin(t));
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[j], width * sin[j]);
|
||||
if (radialSegments === 2) {
|
||||
// add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t))
|
||||
Vec3.copy(normalVector, v);
|
||||
Vec3.normalize(normalVector, normalVector);
|
||||
if (t !== 0 || i % 2 === 0) Vec3.negate(normalVector, normalVector);
|
||||
v3copy(normalVector, v);
|
||||
v3normalize(normalVector, normalVector);
|
||||
if (j !== 0 || i % 2 === 0) v3negate(normalVector, normalVector);
|
||||
} else {
|
||||
add2AndScale2(normalVector, u, v, w * Math.cos(t), h * Math.sin(t));
|
||||
add2AndScale2(normalVector, u, v, width * cos[j], height * sin[j]);
|
||||
}
|
||||
Vec3.normalize(normalVector, normalVector);
|
||||
v3normalize(normalVector, normalVector);
|
||||
|
||||
ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < linearSegments; ++i) {
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i * radialSegments + (j + 1) % radialSegments,
|
||||
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
|
||||
vertexCount + i * radialSegments + j
|
||||
);
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
|
||||
vertexCount + (i + 1) * radialSegments + j,
|
||||
@@ -85,27 +102,25 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
if (startCap) {
|
||||
const offset = 0;
|
||||
const centerVertex = vertices.elementCount;
|
||||
Vec3.fromArray(u, normalVectors, offset);
|
||||
Vec3.fromArray(v, binormalVectors, offset);
|
||||
Vec3.fromArray(controlPoint, controlPoints, offset);
|
||||
Vec3.cross(normalVector, v, u);
|
||||
v3fromArray(u, normalVectors, offset);
|
||||
v3fromArray(v, binormalVectors, offset);
|
||||
v3fromArray(controlPoint, controlPoints, offset);
|
||||
v3cross(normalVector, v, u);
|
||||
|
||||
ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
const width = widthValues[0];
|
||||
const height = heightValues[0];
|
||||
|
||||
vertexCount = vertices.elementCount;
|
||||
for (let i = 0; i < radialSegments; ++i) {
|
||||
const t = 2 * Math.PI * i / radialSegments;
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
|
||||
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, height * Math.cos(t), width * Math.sin(t));
|
||||
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + (i + 1) % radialSegments,
|
||||
vertexCount + i,
|
||||
@@ -117,27 +132,25 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
if (endCap) {
|
||||
const offset = linearSegments * 3;
|
||||
const centerVertex = vertices.elementCount;
|
||||
Vec3.fromArray(u, normalVectors, offset);
|
||||
Vec3.fromArray(v, binormalVectors, offset);
|
||||
Vec3.fromArray(controlPoint, controlPoints, offset);
|
||||
Vec3.cross(normalVector, u, v);
|
||||
v3fromArray(u, normalVectors, offset);
|
||||
v3fromArray(v, binormalVectors, offset);
|
||||
v3fromArray(controlPoint, controlPoints, offset);
|
||||
v3cross(normalVector, u, v);
|
||||
|
||||
ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
const width = widthValues[linearSegments];
|
||||
const height = heightValues[linearSegments];
|
||||
|
||||
vertexCount = vertices.elementCount;
|
||||
for (let i = 0; i < radialSegments; ++i) {
|
||||
const t = 2 * Math.PI * i / radialSegments;
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, height * cos[i], width * sin[i]);
|
||||
|
||||
add3AndScale2(surfacePoint, u, v, controlPoint, height * Math.cos(t), width * Math.sin(t));
|
||||
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
ChunkedArray.add3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
ChunkedArray.add3(
|
||||
caAdd3(
|
||||
indices,
|
||||
vertexCount + i,
|
||||
vertexCount + (i + 1) % radialSegments,
|
||||
|
||||
@@ -12,12 +12,22 @@ import { Cage } from '../../../mol-geo/primitive/cage';
|
||||
import { addSphere } from './builder/sphere';
|
||||
import { addCylinder } from './builder/cylinder';
|
||||
|
||||
const tmpV = Vec3.zero();
|
||||
const tmpMat3 = Mat3.zero();
|
||||
const tmpVecA = Vec3.zero();
|
||||
const tmpVecB = Vec3.zero();
|
||||
const tmpVecC = Vec3.zero();
|
||||
const tmpVecD = Vec3.zero();
|
||||
const tmpV = Vec3();
|
||||
const tmpMat3 = Mat3();
|
||||
const tmpVecA = Vec3();
|
||||
const tmpVecB = Vec3();
|
||||
const tmpVecC = Vec3();
|
||||
const tmpVecD = Vec3();
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3triangleNormal = Vec3.triangleNormal;
|
||||
const v3copy = Vec3.copy;
|
||||
const v3transformMat4 = Vec3.transformMat4;
|
||||
const v3transformMat3 = Vec3.transformMat3;
|
||||
const mat3directionTransform = Mat3.directionTransform;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
export namespace MeshBuilder {
|
||||
export interface State {
|
||||
@@ -45,36 +55,36 @@ export namespace MeshBuilder {
|
||||
const offset = vertices.elementCount;
|
||||
|
||||
// positions
|
||||
ChunkedArray.add3(vertices, a[0], a[1], a[2]);
|
||||
ChunkedArray.add3(vertices, b[0], b[1], b[2]);
|
||||
ChunkedArray.add3(vertices, c[0], c[1], c[2]);
|
||||
caAdd3(vertices, a[0], a[1], a[2]);
|
||||
caAdd3(vertices, b[0], b[1], b[2]);
|
||||
caAdd3(vertices, c[0], c[1], c[2]);
|
||||
|
||||
Vec3.triangleNormal(tmpV, a, b, c);
|
||||
v3triangleNormal(tmpV, a, b, c);
|
||||
for (let i = 0; i < 3; ++i) {
|
||||
ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]); // normal
|
||||
ChunkedArray.add(groups, currentGroup); // group
|
||||
caAdd3(normals, tmpV[0], tmpV[1], tmpV[2]); // normal
|
||||
caAdd(groups, currentGroup); // group
|
||||
}
|
||||
ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
|
||||
caAdd3(indices, offset, offset + 1, offset + 2);
|
||||
}
|
||||
|
||||
export function addTriangleStrip(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
|
||||
Vec3.fromArray(tmpVecC, vertices, indices[0] * 3);
|
||||
Vec3.fromArray(tmpVecD, vertices, indices[1] * 3);
|
||||
v3fromArray(tmpVecC, vertices, indices[0] * 3);
|
||||
v3fromArray(tmpVecD, vertices, indices[1] * 3);
|
||||
for (let i = 2, il = indices.length; i < il; i += 2) {
|
||||
Vec3.copy(tmpVecA, tmpVecC);
|
||||
Vec3.copy(tmpVecB, tmpVecD);
|
||||
Vec3.fromArray(tmpVecC, vertices, indices[i] * 3);
|
||||
Vec3.fromArray(tmpVecD, vertices, indices[i + 1] * 3);
|
||||
v3copy(tmpVecA, tmpVecC);
|
||||
v3copy(tmpVecB, tmpVecD);
|
||||
v3fromArray(tmpVecC, vertices, indices[i] * 3);
|
||||
v3fromArray(tmpVecD, vertices, indices[i + 1] * 3);
|
||||
addTriangle(state, tmpVecA, tmpVecB, tmpVecC);
|
||||
addTriangle(state, tmpVecB, tmpVecD, tmpVecC);
|
||||
}
|
||||
}
|
||||
|
||||
export function addTriangleFan(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
|
||||
Vec3.fromArray(tmpVecA, vertices, indices[0] * 3);
|
||||
v3fromArray(tmpVecA, vertices, indices[0] * 3);
|
||||
for (let i = 2, il = indices.length; i < il; ++i) {
|
||||
Vec3.fromArray(tmpVecB, vertices, indices[i - 1] * 3);
|
||||
Vec3.fromArray(tmpVecC, vertices, indices[i] * 3);
|
||||
v3fromArray(tmpVecB, vertices, indices[i - 1] * 3);
|
||||
v3fromArray(tmpVecC, vertices, indices[i] * 3);
|
||||
addTriangle(state, tmpVecA, tmpVecC, tmpVecB);
|
||||
}
|
||||
}
|
||||
@@ -83,19 +93,19 @@ export namespace MeshBuilder {
|
||||
const { vertices: va, normals: na, indices: ia } = primitive;
|
||||
const { vertices, normals, indices, groups, currentGroup } = state;
|
||||
const offset = vertices.elementCount;
|
||||
const n = Mat3.directionTransform(tmpMat3, t);
|
||||
const n = mat3directionTransform(tmpMat3, t);
|
||||
for (let i = 0, il = va.length; i < il; i += 3) {
|
||||
// position
|
||||
Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t);
|
||||
ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, va, i), t);
|
||||
caAdd3(vertices, tmpV[0], tmpV[1], tmpV[2]);
|
||||
// normal
|
||||
Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n);
|
||||
ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, na, i), n);
|
||||
caAdd3(normals, tmpV[0], tmpV[1], tmpV[2]);
|
||||
// group
|
||||
ChunkedArray.add(groups, currentGroup);
|
||||
caAdd(groups, currentGroup);
|
||||
}
|
||||
for (let i = 0, il = ia.length; i < il; i += 3) {
|
||||
ChunkedArray.add3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
|
||||
caAdd3(indices, ia[i] + offset, ia[i + 1] + offset, ia[i + 2] + offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,19 +114,19 @@ export namespace MeshBuilder {
|
||||
const { vertices: va, normals: na, indices: ia } = primitive;
|
||||
const { vertices, normals, indices, groups, currentGroup } = state;
|
||||
const offset = vertices.elementCount;
|
||||
const n = Mat3.directionTransform(tmpMat3, t);
|
||||
const n = mat3directionTransform(tmpMat3, t);
|
||||
for (let i = 0, il = va.length; i < il; i += 3) {
|
||||
// position
|
||||
Vec3.transformMat4(tmpV, Vec3.fromArray(tmpV, va, i), t);
|
||||
ChunkedArray.add3(vertices, tmpV[0], tmpV[1], tmpV[2]);
|
||||
v3transformMat4(tmpV, v3fromArray(tmpV, va, i), t);
|
||||
caAdd3(vertices, tmpV[0], tmpV[1], tmpV[2]);
|
||||
// normal
|
||||
Vec3.transformMat3(tmpV, Vec3.fromArray(tmpV, na, i), n);
|
||||
ChunkedArray.add3(normals, -tmpV[0], -tmpV[1], -tmpV[2]);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, na, i), n);
|
||||
caAdd3(normals, -tmpV[0], -tmpV[1], -tmpV[2]);
|
||||
// group
|
||||
ChunkedArray.add(groups, currentGroup);
|
||||
caAdd(groups, currentGroup);
|
||||
}
|
||||
for (let i = 0, il = ia.length; i < il; i += 3) {
|
||||
ChunkedArray.add3(indices, ia[i + 2] + offset, ia[i + 1] + offset, ia[i] + offset);
|
||||
caAdd3(indices, ia[i + 2] + offset, ia[i + 1] + offset, ia[i] + offset);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,10 +134,10 @@ export namespace MeshBuilder {
|
||||
const { vertices: va, edges: ea } = cage;
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments };
|
||||
for (let i = 0, il = ea.length; i < il; i += 2) {
|
||||
Vec3.fromArray(tmpVecA, va, ea[i] * 3);
|
||||
Vec3.fromArray(tmpVecB, va, ea[i + 1] * 3);
|
||||
Vec3.transformMat4(tmpVecA, tmpVecA, t);
|
||||
Vec3.transformMat4(tmpVecB, tmpVecB, t);
|
||||
v3fromArray(tmpVecA, va, ea[i] * 3);
|
||||
v3fromArray(tmpVecB, va, ea[i + 1] * 3);
|
||||
v3transformMat4(tmpVecA, tmpVecA, t);
|
||||
v3transformMat4(tmpVecB, tmpVecB, t);
|
||||
addSphere(state, tmpVecA, radius, detail);
|
||||
addSphere(state, tmpVecB, radius, detail);
|
||||
addCylinder(state, tmpVecA, tmpVecB, 1, cylinderProps);
|
||||
|
||||
@@ -12,7 +12,7 @@ import { transformPositionArray, transformDirectionArray, computeIndexedVertexNo
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
import { TransformData } from '../transform-data';
|
||||
import { LocationIterator } from '../../util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../util/location-iterator';
|
||||
import { createColors } from '../color-data';
|
||||
import { ChunkedArray, hashFnv32a } from '../../../mol-data/util';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -24,6 +24,7 @@ import { BaseGeometry } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
|
||||
export interface Mesh {
|
||||
readonly kind: 'mesh',
|
||||
@@ -332,6 +333,7 @@ export namespace Mesh {
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -342,23 +344,43 @@ export namespace Mesh {
|
||||
createValuesSimple,
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState: BaseGeometry.createRenderableState,
|
||||
updateRenderableState: BaseGeometry.updateRenderableState
|
||||
createRenderableState,
|
||||
updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(mesh: Mesh, transform: TransformData): LocationIterator {
|
||||
const groupCount = mesh.vertexCount;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const v = mesh.vertexBuffer.ref.value;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (instanceIndex < 0) {
|
||||
Vec3.fromArray(p, v, groupIndex * 3);
|
||||
} else {
|
||||
Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function createValues(mesh: Mesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): MeshValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(mesh, transform);
|
||||
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount };
|
||||
const counts = { drawCount: mesh.triangleCount * 3, vertexCount: mesh.vertexCount, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(mesh.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
@@ -383,6 +405,7 @@ export namespace Mesh {
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -398,6 +421,7 @@ export namespace Mesh {
|
||||
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
|
||||
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {
|
||||
@@ -412,4 +436,16 @@ export namespace Mesh {
|
||||
ValueCell.update(values.uInvariantBoundingSphere, Vec4.fromSphere(values.uInvariantBoundingSphere.ref.value, invariantBoundingSphere));
|
||||
}
|
||||
}
|
||||
|
||||
function createRenderableState(props: PD.Values<Params>): RenderableState {
|
||||
const state = BaseGeometry.createRenderableState(props);
|
||||
updateRenderableState(state, props);
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
|
||||
BaseGeometry.updateRenderableState(state, props);
|
||||
state.opaque = state.opaque && !props.xrayShaded;
|
||||
state.writeDepth = state.opaque;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -33,7 +33,7 @@ export function createOverpaint(count: number, overpaintData?: OverpaintData): O
|
||||
if (overpaintData) {
|
||||
ValueCell.update(overpaintData.tOverpaint, overpaint);
|
||||
ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(overpaint.width, overpaint.height));
|
||||
ValueCell.update(overpaintData.dOverpaint, count > 0);
|
||||
ValueCell.updateIfChanged(overpaintData.dOverpaint, count > 0);
|
||||
return overpaintData;
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -15,8 +15,3 @@ export namespace PickingId {
|
||||
return a.objectId === b.objectId && a.instanceId === b.instanceId && a.groupId === b.groupId;
|
||||
}
|
||||
}
|
||||
|
||||
export interface PickingInfo {
|
||||
label: string
|
||||
data?: any
|
||||
}
|
||||
@@ -7,6 +7,10 @@
|
||||
import { ChunkedArray } from '../../../mol-data/util';
|
||||
import { Points } from './points';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
export interface PointsBuilder {
|
||||
add(x: number, y: number, z: number, group: number): void
|
||||
getPoints(): Points
|
||||
@@ -19,8 +23,8 @@ export namespace PointsBuilder {
|
||||
|
||||
return {
|
||||
add: (x: number, y: number, z: number, group: number) => {
|
||||
ChunkedArray.add3(centers, x, y, z);
|
||||
ChunkedArray.add(groups, group);
|
||||
caAdd3(centers, x, y, z);
|
||||
caAdd(groups, group);
|
||||
},
|
||||
getPoints: () => {
|
||||
const cb = ChunkedArray.compact(centers, true) as Float32Array;
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Mat4, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Mat4, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { transformPositionArray, GroupMapping, createGroupMapping} from '../../util';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createColors } from '../color-data';
|
||||
import { createMarkers } from '../marker-data';
|
||||
import { createSizes } from '../size-data';
|
||||
import { TransformData } from '../transform-data';
|
||||
import { LocationIterator } from '../../util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../util/location-iterator';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } from '../../../mol-gl/renderable/util';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
@@ -134,19 +134,40 @@ export namespace Points {
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState,
|
||||
updateRenderableState
|
||||
updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(points: Points, transform: TransformData): LocationIterator {
|
||||
const groupCount = points.pointCount;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const v = points.centerBuffer.ref.value;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (instanceIndex < 0) {
|
||||
Vec3.fromArray(p, v, groupIndex * 3);
|
||||
} else {
|
||||
Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 1, getLocation);
|
||||
}
|
||||
|
||||
function createValues(points: Points, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): PointsValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const positionIt = createPositionIterator(points, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: points.pointCount, groupCount, instanceCount };
|
||||
const counts = { drawCount: points.pointCount, vertexCount: points.pointCount, groupCount, instanceCount };
|
||||
|
||||
const invariantBoundingSphere = Sphere3D.clone(points.boundingSphere);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -11,7 +11,7 @@ import { LocationIterator } from '../util/location-iterator';
|
||||
import { Location, NullLocation } from '../../mol-model/location';
|
||||
import { SizeTheme } from '../../mol-theme/size';
|
||||
import { Geometry } from './geometry';
|
||||
import { encodeFloatLog, decodeFloatLog } from '../../mol-util/float-packing';
|
||||
import { decodeFloatRGB, encodeFloatRGBtoArray } from '../../mol-util/float-packing';
|
||||
|
||||
export type SizeType = 'uniform' | 'instance' | 'group' | 'groupInstance'
|
||||
|
||||
@@ -31,6 +31,8 @@ export function createSizes(locationIt: LocationIterator, sizeTheme: SizeTheme<a
|
||||
}
|
||||
}
|
||||
|
||||
const sizeFactor = 100; // NOTE same factor is set in shaders
|
||||
|
||||
export function getMaxSize(sizeData: SizeData): number {
|
||||
const type = sizeData.dSizeType.ref.value as SizeType;
|
||||
switch (type) {
|
||||
@@ -41,17 +43,17 @@ export function getMaxSize(sizeData: SizeData): number {
|
||||
case 'groupInstance':
|
||||
let maxSize = 0;
|
||||
const array = sizeData.tSize.ref.value.array;
|
||||
for (let i = 0, il = array.length; i < il; ++i) {
|
||||
const value = decodeFloatLog(array[i] / 255);
|
||||
for (let i = 0, il = array.length; i < il; i += 3) {
|
||||
const value = decodeFloatRGB(array[i], array[i + 1], array[i + 2]);
|
||||
if (maxSize < value) maxSize = value;
|
||||
}
|
||||
return maxSize;
|
||||
return maxSize / sizeFactor;
|
||||
}
|
||||
}
|
||||
|
||||
export type LocationSize = (location: Location) => number
|
||||
|
||||
const emptySizeTexture = { array: new Uint8Array(1), width: 1, height: 1 };
|
||||
const emptySizeTexture = { array: new Uint8Array(3), width: 1, height: 1 };
|
||||
function createEmptySizeTexture() {
|
||||
return {
|
||||
tSize: ValueCell.create(emptySizeTexture),
|
||||
@@ -62,9 +64,7 @@ function createEmptySizeTexture() {
|
||||
export function createValueSize(value: number, sizeData?: SizeData): SizeData {
|
||||
if (sizeData) {
|
||||
ValueCell.update(sizeData.uSize, value);
|
||||
if (sizeData.dSizeType.ref.value !== 'uniform') {
|
||||
ValueCell.update(sizeData.dSizeType, 'uniform');
|
||||
}
|
||||
ValueCell.updateIfChanged(sizeData.dSizeType, 'uniform');
|
||||
return sizeData;
|
||||
} else {
|
||||
return {
|
||||
@@ -84,9 +84,7 @@ export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeTyp
|
||||
if (sizeData) {
|
||||
ValueCell.update(sizeData.tSize, sizes);
|
||||
ValueCell.update(sizeData.uSizeTexDim, Vec2.create(sizes.width, sizes.height));
|
||||
if (sizeData.dSizeType.ref.value !== type) {
|
||||
ValueCell.update(sizeData.dSizeType, type);
|
||||
}
|
||||
ValueCell.updateIfChanged(sizeData.dSizeType, type);
|
||||
return sizeData;
|
||||
} else {
|
||||
return {
|
||||
@@ -101,11 +99,11 @@ export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeTyp
|
||||
/** Creates size texture with size for each instance/unit */
|
||||
export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
|
||||
const { instanceCount} = locationIt;
|
||||
const sizes = createTextureImage(Math.max(1, instanceCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
const sizes = createTextureImage(Math.max(1, instanceCount), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
const v = locationIt.move();
|
||||
sizes.array[v.instanceIndex] = encodeFloatLog(sizeFn(v.location)) * 255;
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.instanceIndex * 3);
|
||||
locationIt.skipInstance();
|
||||
}
|
||||
return createTextureSize(sizes, 'instance', sizeData);
|
||||
@@ -114,11 +112,11 @@ export function createInstanceSize(locationIt: LocationIterator, sizeFn: Locatio
|
||||
/** Creates size texture with size for each group (i.e. shared across instances/units) */
|
||||
export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
|
||||
const { groupCount } = locationIt;
|
||||
const sizes = createTextureImage(Math.max(1, groupCount), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
const sizes = createTextureImage(Math.max(1, groupCount), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
const v = locationIt.move();
|
||||
sizes.array[v.groupIndex] = encodeFloatLog(sizeFn(v.location)) * 255;
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.groupIndex * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'group', sizeData);
|
||||
}
|
||||
@@ -127,11 +125,11 @@ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSi
|
||||
export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
|
||||
const { groupCount, instanceCount } = locationIt;
|
||||
const count = instanceCount * groupCount;
|
||||
const sizes = createTextureImage(Math.max(1, count), 1, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
const sizes = createTextureImage(Math.max(1, count), 3, Uint8Array, sizeData && sizeData.tSize.ref.value.array);
|
||||
locationIt.reset();
|
||||
while (locationIt.hasNext && !locationIt.isNextNewInstance) {
|
||||
while (locationIt.hasNext) {
|
||||
const v = locationIt.move();
|
||||
sizes.array[v.index] = encodeFloatLog(sizeFn(v.location)) * 255;
|
||||
encodeFloatRGBtoArray(sizeFn(v.location) * sizeFactor, sizes.array, v.index * 3);
|
||||
}
|
||||
return createTextureSize(sizes, 'groupInstance', sizeData);
|
||||
}
|
||||
@@ -19,6 +19,11 @@ const quadIndices = new Uint16Array([
|
||||
1, 3, 2
|
||||
]);
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd2 = ChunkedArray.add2;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
export interface SpheresBuilder {
|
||||
add(x: number, y: number, z: number, group: number): void
|
||||
getSpheres(): Spheres
|
||||
@@ -37,12 +42,12 @@ export namespace SpheresBuilder {
|
||||
add: (x: number, y: number, z: number, group: number) => {
|
||||
const offset = centers.elementCount;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
ChunkedArray.add3(centers, x, y, z);
|
||||
ChunkedArray.add2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1]);
|
||||
ChunkedArray.add(groups, group);
|
||||
caAdd3(centers, x, y, z);
|
||||
caAdd2(mappings, quadMapping[i * 2], quadMapping[i * 2 + 1]);
|
||||
caAdd(groups, group);
|
||||
}
|
||||
ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
},
|
||||
getSpheres: () => {
|
||||
const cb = ChunkedArray.compact(centers, true) as Float32Array;
|
||||
|
||||
@@ -8,7 +8,7 @@ import { ValueCell } from '../../../mol-util';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TransformData } from '../transform-data';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../../mol-geo/util/location-iterator';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { SpheresValues } from '../../../mol-gl/renderable/spheres';
|
||||
import { createColors } from '../color-data';
|
||||
@@ -23,7 +23,8 @@ import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { GroupMapping, createGroupMapping } from '../../util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
|
||||
export interface Spheres {
|
||||
readonly kind: 'spheres',
|
||||
@@ -111,10 +112,12 @@ export namespace Spheres {
|
||||
}
|
||||
|
||||
function update(centers: Float32Array, mappings: Float32Array, indices: Uint32Array, groups: Float32Array, sphereCount: number, spheres: Spheres) {
|
||||
if (sphereCount > spheres.sphereCount) {
|
||||
ValueCell.update(spheres.mappingBuffer, mappings);
|
||||
ValueCell.update(spheres.indexBuffer, indices);
|
||||
}
|
||||
spheres.sphereCount = sphereCount;
|
||||
ValueCell.update(spheres.centerBuffer, centers);
|
||||
ValueCell.update(spheres.mappingBuffer, mappings);
|
||||
ValueCell.update(spheres.indexBuffer, indices);
|
||||
ValueCell.update(spheres.groupBuffer, groups);
|
||||
return spheres;
|
||||
}
|
||||
@@ -124,6 +127,7 @@ export namespace Spheres {
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -134,26 +138,46 @@ export namespace Spheres {
|
||||
createValuesSimple,
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState: BaseGeometry.createRenderableState,
|
||||
updateRenderableState: BaseGeometry.updateRenderableState
|
||||
createRenderableState,
|
||||
updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(spheres: Spheres, transform: TransformData): LocationIterator {
|
||||
const groupCount = spheres.sphereCount * 4;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const v = spheres.centerBuffer.ref.value;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (instanceIndex < 0) {
|
||||
Vec3.fromArray(p, v, groupIndex * 3);
|
||||
} else {
|
||||
Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 4, getLocation);
|
||||
}
|
||||
|
||||
function createValues(spheres: Spheres, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): SpheresValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(spheres, transform);
|
||||
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount };
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 4, groupCount, instanceCount };
|
||||
|
||||
const padding = getMaxSize(size);
|
||||
const padding = getMaxSize(size) * props.sizeFactor;
|
||||
const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), spheres.boundingSphere, padding);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, transform.aTransform.ref.value, instanceCount);
|
||||
|
||||
@@ -179,6 +203,7 @@ export namespace Spheres {
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor),
|
||||
dDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -193,10 +218,11 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.dDoubleSided, props.doubleSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: SpheresValues, spheres: Spheres) {
|
||||
const padding = getMaxSize(values);
|
||||
const padding = getMaxSize(values) * values.uSizeFactor.ref.value;
|
||||
const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), spheres.boundingSphere, padding);
|
||||
const boundingSphere = calculateTransformBoundingSphere(invariantBoundingSphere, values.aTransform.ref.value, values.instanceCount.ref.value);
|
||||
|
||||
@@ -209,4 +235,16 @@ export namespace Spheres {
|
||||
}
|
||||
ValueCell.update(values.padding, padding);
|
||||
}
|
||||
|
||||
function createRenderableState(props: PD.Values<Params>): RenderableState {
|
||||
const state = BaseGeometry.createRenderableState(props);
|
||||
updateRenderableState(state, props);
|
||||
return state;
|
||||
}
|
||||
|
||||
function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
|
||||
BaseGeometry.updateRenderableState(state, props);
|
||||
state.opaque = state.opaque && !props.xrayShaded;
|
||||
state.writeDepth = state.opaque;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,11 @@ const quadIndices = new Uint16Array([
|
||||
1, 3, 2
|
||||
]);
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
const caAdd2 = ChunkedArray.add2;
|
||||
const caAdd = ChunkedArray.add;
|
||||
|
||||
export interface TextBuilder {
|
||||
add(str: string, x: number, y: number, z: number, depth: number, scale: number, group: number): void
|
||||
getText(): Text
|
||||
@@ -38,9 +43,9 @@ export namespace TextBuilder {
|
||||
const outline = fontAtlas.buffer / fontAtlas.lineHeight;
|
||||
|
||||
const add = (x: number, y: number, z: number, depth: number, group: number) => {
|
||||
ChunkedArray.add3(centers, x, y, z);
|
||||
ChunkedArray.add(depths, depth);
|
||||
ChunkedArray.add(groups, group);
|
||||
caAdd3(centers, x, y, z);
|
||||
caAdd(depths, depth);
|
||||
caAdd(groups, group);
|
||||
};
|
||||
|
||||
return {
|
||||
@@ -117,18 +122,18 @@ export namespace TextBuilder {
|
||||
|
||||
// background
|
||||
if (background) {
|
||||
ChunkedArray.add2(mappings, xLeft, yTop); // top left
|
||||
ChunkedArray.add2(mappings, xLeft, yBottom); // bottom left
|
||||
ChunkedArray.add2(mappings, xRight, yTop); // top right
|
||||
ChunkedArray.add2(mappings, xRight, yBottom); // bottom right
|
||||
caAdd2(mappings, xLeft, yTop); // top left
|
||||
caAdd2(mappings, xLeft, yBottom); // bottom left
|
||||
caAdd2(mappings, xRight, yTop); // top right
|
||||
caAdd2(mappings, xRight, yBottom); // bottom right
|
||||
|
||||
const offset = centers.elementCount;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
ChunkedArray.add2(tcoords, 10, 10);
|
||||
caAdd2(tcoords, 10, 10);
|
||||
add(x, y, z, depth, group);
|
||||
}
|
||||
ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
}
|
||||
|
||||
if (tether) {
|
||||
@@ -234,18 +239,18 @@ export namespace TextBuilder {
|
||||
default:
|
||||
throw new Error('unsupported attachment');
|
||||
}
|
||||
ChunkedArray.add2(mappings, xTip, yTip); // tip
|
||||
ChunkedArray.add2(mappings, xBaseA, yBaseA); // base A
|
||||
ChunkedArray.add2(mappings, xBaseB, yBaseB); // base B
|
||||
ChunkedArray.add2(mappings, xBaseCenter, yBaseCenter); // base center
|
||||
caAdd2(mappings, xTip, yTip); // tip
|
||||
caAdd2(mappings, xBaseA, yBaseA); // base A
|
||||
caAdd2(mappings, xBaseB, yBaseB); // base B
|
||||
caAdd2(mappings, xBaseCenter, yBaseCenter); // base center
|
||||
|
||||
const offset = centers.elementCount;
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
ChunkedArray.add2(tcoords, 10, 10);
|
||||
caAdd2(tcoords, 10, 10);
|
||||
add(x, y, z, depth, group);
|
||||
}
|
||||
ChunkedArray.add3(indices, offset, offset + 1, offset + 3);
|
||||
ChunkedArray.add3(indices, offset, offset + 3, offset + 2);
|
||||
caAdd3(indices, offset, offset + 1, offset + 3);
|
||||
caAdd3(indices, offset, offset + 3, offset + 2);
|
||||
}
|
||||
|
||||
xShift += outline;
|
||||
@@ -260,25 +265,25 @@ export namespace TextBuilder {
|
||||
const top = (c.nh - yShift) * scale;
|
||||
const bottom = (-yShift) * scale;
|
||||
|
||||
ChunkedArray.add2(mappings, left, top);
|
||||
ChunkedArray.add2(mappings, left, bottom);
|
||||
ChunkedArray.add2(mappings, right, top);
|
||||
ChunkedArray.add2(mappings, right, bottom);
|
||||
caAdd2(mappings, left, top);
|
||||
caAdd2(mappings, left, bottom);
|
||||
caAdd2(mappings, right, top);
|
||||
caAdd2(mappings, right, bottom);
|
||||
|
||||
const texWidth = fontAtlas.texture.width;
|
||||
const texHeight = fontAtlas.texture.height;
|
||||
|
||||
ChunkedArray.add2(tcoords, c.x / texWidth, c.y / texHeight); // top left
|
||||
ChunkedArray.add2(tcoords, c.x / texWidth, (c.y + c.h) / texHeight); // bottom left
|
||||
ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, c.y / texHeight); // top right
|
||||
ChunkedArray.add2(tcoords, (c.x + c.w) / texWidth, (c.y + c.h) / texHeight); // bottom right
|
||||
caAdd2(tcoords, c.x / texWidth, c.y / texHeight); // top left
|
||||
caAdd2(tcoords, c.x / texWidth, (c.y + c.h) / texHeight); // bottom left
|
||||
caAdd2(tcoords, (c.x + c.w) / texWidth, c.y / texHeight); // top right
|
||||
caAdd2(tcoords, (c.x + c.w) / texWidth, (c.y + c.h) / texHeight); // bottom right
|
||||
|
||||
xadvance += c.nw - 2 * outline;
|
||||
|
||||
const offset = centers.elementCount;
|
||||
for (let i = 0; i < 4; ++i) add(x, y, z, depth, group);
|
||||
ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
caAdd3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2]);
|
||||
caAdd3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5]);
|
||||
}
|
||||
},
|
||||
getText: () => {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { LocationIterator, PositionLocation } from '../../../mol-geo/util/location-iterator';
|
||||
import { TransformData } from '../transform-data';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { createColors } from '../color-data';
|
||||
@@ -183,22 +183,42 @@ export namespace Text {
|
||||
updateBoundingSphere,
|
||||
createRenderableState,
|
||||
updateRenderableState,
|
||||
createPositionIterator
|
||||
};
|
||||
|
||||
function createPositionIterator(text: Text, transform: TransformData): LocationIterator {
|
||||
const groupCount = text.charCount * 4;
|
||||
const instanceCount = transform.instanceCount.ref.value;
|
||||
const location = PositionLocation();
|
||||
const p = location.position;
|
||||
const v = text.centerBuffer.ref.value;
|
||||
const m = transform.aTransform.ref.value;
|
||||
const getLocation = (groupIndex: number, instanceIndex: number) => {
|
||||
if (instanceIndex < 0) {
|
||||
Vec3.fromArray(p, v, groupIndex * 3);
|
||||
} else {
|
||||
Vec3.transformMat4Offset(p, v, m, 0, groupIndex * 3, instanceIndex * 16);
|
||||
}
|
||||
return location;
|
||||
};
|
||||
return LocationIterator(groupCount, instanceCount, 4, getLocation);
|
||||
}
|
||||
|
||||
function createValues(text: Text, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
if (instanceCount !== transform.instanceCount.ref.value) {
|
||||
throw new Error('instanceCount values in TransformData and LocationIterator differ');
|
||||
}
|
||||
const positionIt = createPositionIterator(text, transform);
|
||||
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, theme.size);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount };
|
||||
const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
|
||||
|
||||
const padding = getPadding(text.mappingBuffer.ref.value, text.depthBuffer.ref.value, text.charCount, getMaxSize(size));
|
||||
const invariantBoundingSphere = Sphere3D.expand(Sphere3D(), text.boundingSphere, padding);
|
||||
|
||||
@@ -23,6 +23,7 @@ import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { Vec2, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { fillSerial } from '../../../mol-util/array';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { NullLocation } from '../../../mol-model/location';
|
||||
|
||||
export interface TextureMesh {
|
||||
readonly kind: 'texture-mesh',
|
||||
@@ -85,18 +86,21 @@ export namespace TextureMesh {
|
||||
updateValues,
|
||||
updateBoundingSphere,
|
||||
createRenderableState: BaseGeometry.createRenderableState,
|
||||
updateRenderableState: BaseGeometry.updateRenderableState
|
||||
updateRenderableState: BaseGeometry.updateRenderableState,
|
||||
createPositionIterator: () => LocationIterator(1, 1, 1, () => NullLocation)
|
||||
};
|
||||
|
||||
function createValues(textureMesh: TextureMesh, transform: TransformData, locationIt: LocationIterator, theme: Theme, props: PD.Values<Params>): TextureMeshValues {
|
||||
const { instanceCount, groupCount } = locationIt;
|
||||
const color = createColors(locationIt, theme.color);
|
||||
const positionIt = Utils.createPositionIterator(textureMesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = createMarkers(instanceCount * groupCount);
|
||||
const overpaint = createEmptyOverpaint();
|
||||
const transparency = createEmptyTransparency();
|
||||
const clipping = createEmptyClipping();
|
||||
|
||||
const counts = { drawCount: textureMesh.vertexCount, groupCount, instanceCount };
|
||||
const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount / 3, groupCount, instanceCount };
|
||||
|
||||
const transformBoundingSphere = calculateTransformBoundingSphere(textureMesh.boundingSphere, transform.aTransform.ref.value, transform.instanceCount.ref.value);
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra';
|
||||
import { Mat4, Mat3 } from '../../mol-math/linear-algebra';
|
||||
import { fillSerial } from '../../mol-util/array';
|
||||
|
||||
export type TransformData = {
|
||||
@@ -24,14 +24,28 @@ export type TransformData = {
|
||||
uInstanceCount: ValueCell<number>,
|
||||
instanceCount: ValueCell<number>,
|
||||
aInstance: ValueCell<Float32Array>,
|
||||
|
||||
hasReflection: ValueCell<boolean>,
|
||||
}
|
||||
|
||||
const _m3 = Mat3();
|
||||
const _m4 = Mat4();
|
||||
function checkReflection(transformArray: Float32Array, instanceCount: number) {
|
||||
for (let i = 0; i < instanceCount; i++) {
|
||||
Mat3.fromMat4(_m3, Mat4.fromArray(_m4, transformArray, i * 16));
|
||||
if (Mat3.determinant(_m3) < 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createTransform(transformArray: Float32Array, instanceCount: number, transformData?: TransformData): TransformData {
|
||||
const hasReflection = checkReflection(transformArray, instanceCount);
|
||||
|
||||
if (transformData) {
|
||||
ValueCell.update(transformData.matrix, transformData.matrix.ref.value);
|
||||
ValueCell.update(transformData.transform, transformArray);
|
||||
ValueCell.update(transformData.uInstanceCount, instanceCount);
|
||||
ValueCell.update(transformData.instanceCount, instanceCount);
|
||||
ValueCell.updateIfChanged(transformData.uInstanceCount, instanceCount);
|
||||
ValueCell.updateIfChanged(transformData.instanceCount, instanceCount);
|
||||
|
||||
const aTransform = transformData.aTransform.ref.value.length >= instanceCount * 16 ? transformData.aTransform.ref.value : new Float32Array(instanceCount * 16);
|
||||
aTransform.set(transformArray);
|
||||
@@ -44,6 +58,8 @@ export function createTransform(transformArray: Float32Array, instanceCount: num
|
||||
const aInstance = transformData.aInstance.ref.value.length >= instanceCount ? transformData.aInstance.ref.value : new Float32Array(instanceCount);
|
||||
ValueCell.update(transformData.aInstance, fillSerial(aInstance, instanceCount));
|
||||
|
||||
ValueCell.update(transformData.hasReflection, hasReflection);
|
||||
|
||||
updateTransformData(transformData);
|
||||
return transformData;
|
||||
} else {
|
||||
@@ -54,7 +70,8 @@ export function createTransform(transformArray: Float32Array, instanceCount: num
|
||||
extraTransform: ValueCell.create(fillIdentityTransform(new Float32Array(instanceCount * 16), instanceCount)),
|
||||
uInstanceCount: ValueCell.create(instanceCount),
|
||||
instanceCount: ValueCell.create(instanceCount),
|
||||
aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount)))
|
||||
aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
|
||||
hasReflection: ValueCell.create(hasReflection),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ export type TransparencyData = {
|
||||
tTransparency: ValueCell<TextureImage<Uint8Array>>
|
||||
uTransparencyTexDim: ValueCell<Vec2>
|
||||
dTransparency: ValueCell<boolean>,
|
||||
dTransparencyVariant: ValueCell<string>,
|
||||
transparencyAverage: ValueCell<number>,
|
||||
}
|
||||
|
||||
export function applyTransparencyValue(array: Uint8Array, start: number, end: number, value: number) {
|
||||
@@ -22,6 +22,14 @@ export function applyTransparencyValue(array: Uint8Array, start: number, end: nu
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getTransparencyAverage(array: Uint8Array, count: number): number {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < count; ++i) {
|
||||
sum += array[i];
|
||||
}
|
||||
return sum / (255 * count);
|
||||
}
|
||||
|
||||
export function clearTransparency(array: Uint8Array, start: number, end: number) {
|
||||
array.fill(0, start, end);
|
||||
}
|
||||
@@ -31,14 +39,15 @@ export function createTransparency(count: number, transparencyData?: Transparenc
|
||||
if (transparencyData) {
|
||||
ValueCell.update(transparencyData.tTransparency, transparency);
|
||||
ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
|
||||
ValueCell.update(transparencyData.dTransparency, count > 0);
|
||||
ValueCell.updateIfChanged(transparencyData.dTransparency, count > 0);
|
||||
ValueCell.updateIfChanged(transparencyData.transparencyAverage, getTransparencyAverage(transparency.array, count));
|
||||
return transparencyData;
|
||||
} else {
|
||||
return {
|
||||
tTransparency: ValueCell.create(transparency),
|
||||
uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
|
||||
dTransparency: ValueCell.create(count > 0),
|
||||
dTransparencyVariant: ValueCell.create('single'),
|
||||
transparencyAverage: ValueCell.create(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -54,7 +63,7 @@ export function createEmptyTransparency(transparencyData?: TransparencyData): Tr
|
||||
tTransparency: ValueCell.create(emptyTransparencyTexture),
|
||||
uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
dTransparency: ValueCell.create(false),
|
||||
dTransparencyVariant: ValueCell.create('single'),
|
||||
transparencyAverage: ValueCell.create(0),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,14 +9,16 @@ import { Primitive, PrimitiveBuilder } from './primitive';
|
||||
import { polygon } from './polygon';
|
||||
import { Cage, createCage } from './cage';
|
||||
|
||||
const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero();
|
||||
const a = Vec3(), b = Vec3(), c = Vec3(), d = Vec3();
|
||||
const points = polygon(4, true);
|
||||
|
||||
/**
|
||||
* Create a box
|
||||
*/
|
||||
function createBox(perforated: boolean): Primitive {
|
||||
const builder = PrimitiveBuilder(12);
|
||||
const triangleCount = 12;
|
||||
const vertexCount = perforated ? 12 * 3 : 6 * 4;
|
||||
const builder = PrimitiveBuilder(triangleCount, vertexCount);
|
||||
|
||||
// create sides
|
||||
for (let i = 0; i < 4; ++i) {
|
||||
@@ -25,8 +27,11 @@ function createBox(perforated: boolean): Primitive {
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5);
|
||||
Vec3.set(c, points[ni * 3], points[ni * 3 + 1], 0.5);
|
||||
Vec3.set(d, points[i * 3], points[i * 3 + 1], 0.5);
|
||||
builder.add(a, b, c);
|
||||
if (!perforated) builder.add(c, d, a);
|
||||
if (perforated) {
|
||||
builder.add(a, b, c);
|
||||
} else {
|
||||
builder.addQuad(a, b, c, d);
|
||||
}
|
||||
}
|
||||
|
||||
// create bases
|
||||
@@ -34,14 +39,20 @@ function createBox(perforated: boolean): Primitive {
|
||||
Vec3.set(b, points[3], points[4], -0.5);
|
||||
Vec3.set(c, points[6], points[7], -0.5);
|
||||
Vec3.set(d, points[9], points[10], -0.5);
|
||||
builder.add(c, b, a);
|
||||
if (!perforated) builder.add(a, d, c);
|
||||
if (perforated) {
|
||||
builder.add(c, b, a);
|
||||
} else {
|
||||
builder.addQuad(d, c, b, a);
|
||||
}
|
||||
Vec3.set(a, points[0], points[1], 0.5);
|
||||
Vec3.set(b, points[3], points[4], 0.5);
|
||||
Vec3.set(c, points[6], points[7], 0.5);
|
||||
Vec3.set(d, points[9], points[10], 0.5);
|
||||
builder.add(a, b, c);
|
||||
if (!perforated) builder.add(c, d, a);
|
||||
if (perforated) {
|
||||
builder.add(a, b, c);
|
||||
} else {
|
||||
builder.addQuad(a, b, c, d);
|
||||
}
|
||||
|
||||
return builder.getPrimitive();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ export interface Primitive {
|
||||
indices: ArrayLike<number>
|
||||
}
|
||||
|
||||
const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero();
|
||||
const a = Vec3(), b = Vec3(), c = Vec3();
|
||||
|
||||
/** Create primitive with face normals from vertices and indices */
|
||||
export function createPrimitive(vertices: ArrayLike<number>, indices: ArrayLike<number>): Primitive {
|
||||
@@ -39,36 +39,64 @@ export function copyPrimitive(primitive: Primitive): Primitive {
|
||||
|
||||
export interface PrimitiveBuilder {
|
||||
add(a: Vec3, b: Vec3, c: Vec3): void
|
||||
/** Shared vertices and normals, must be flat */
|
||||
addQuad(a: Vec3, b: Vec3, c: Vec3, d: Vec3): void
|
||||
getPrimitive(): Primitive
|
||||
}
|
||||
|
||||
const vn = Vec3.zero();
|
||||
const vn = Vec3();
|
||||
|
||||
/** Builder to create primitive with face normals */
|
||||
export function PrimitiveBuilder(triangleCount: number): PrimitiveBuilder {
|
||||
const vertices = new Float32Array(triangleCount * 3 * 3);
|
||||
const normals = new Float32Array(triangleCount * 3 * 3);
|
||||
export function PrimitiveBuilder(triangleCount: number, vertexCount?: number): PrimitiveBuilder {
|
||||
if (vertexCount === undefined) vertexCount = triangleCount * 3;
|
||||
|
||||
const vertices = new Float32Array(vertexCount * 3);
|
||||
const normals = new Float32Array(vertexCount * 3);
|
||||
const indices = new Uint32Array(triangleCount * 3);
|
||||
let offset = 0;
|
||||
|
||||
let vOffset = 0;
|
||||
let iOffset = 0;
|
||||
|
||||
return {
|
||||
add: (a: Vec3, b: Vec3, c: Vec3) => {
|
||||
Vec3.toArray(a, vertices, offset);
|
||||
Vec3.toArray(b, vertices, offset + 3);
|
||||
Vec3.toArray(c, vertices, offset + 6);
|
||||
Vec3.toArray(a, vertices, vOffset);
|
||||
Vec3.toArray(b, vertices, vOffset + 3);
|
||||
Vec3.toArray(c, vertices, vOffset + 6);
|
||||
Vec3.triangleNormal(vn, a, b, c);
|
||||
for (let j = 0; j < 3; ++j) {
|
||||
Vec3.toArray(vn, normals, offset + 3 * j);
|
||||
indices[offset / 3 + j] = offset / 3 + j;
|
||||
Vec3.toArray(vn, normals, vOffset + 3 * j);
|
||||
indices[iOffset + j] = vOffset / 3 + j;
|
||||
}
|
||||
offset += 9;
|
||||
vOffset += 9;
|
||||
iOffset += 3;
|
||||
},
|
||||
addQuad: (a: Vec3, b: Vec3, c: Vec3, d: Vec3) => {
|
||||
Vec3.toArray(a, vertices, vOffset);
|
||||
Vec3.toArray(b, vertices, vOffset + 3);
|
||||
Vec3.toArray(c, vertices, vOffset + 6);
|
||||
Vec3.toArray(d, vertices, vOffset + 9);
|
||||
Vec3.triangleNormal(vn, a, b, c);
|
||||
for (let j = 0; j < 4; ++j) {
|
||||
Vec3.toArray(vn, normals, vOffset + 3 * j);
|
||||
}
|
||||
const vOffset3 = vOffset / 3;
|
||||
// a, b, c
|
||||
indices[iOffset] = vOffset3;
|
||||
indices[iOffset + 1] = vOffset3 + 1;
|
||||
indices[iOffset + 2] = vOffset3 + 2;
|
||||
// a, b, c
|
||||
indices[iOffset + 3] = vOffset3 + 2;
|
||||
indices[iOffset + 4] = vOffset3 + 3;
|
||||
indices[iOffset + 5] = vOffset3;
|
||||
vOffset += 12;
|
||||
iOffset += 6;
|
||||
},
|
||||
getPrimitive: () => ({ vertices, normals, indices })
|
||||
};
|
||||
}
|
||||
|
||||
const tmpV = Vec3.zero();
|
||||
const tmpMat3 = Mat3.zero();
|
||||
const tmpV = Vec3();
|
||||
const tmpMat3 = Mat3();
|
||||
|
||||
/** Transform primitive in-place */
|
||||
export function transformPrimitive(primitive: Primitive, t: Mat4) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -20,16 +20,30 @@ export const DefaultPrismProps = {
|
||||
export type PrismProps = Partial<typeof DefaultPrismProps>
|
||||
|
||||
/**
|
||||
* Create a prism with a base of 4 or more points
|
||||
* Create a prism with a base of 3 or more points
|
||||
*/
|
||||
export function Prism(points: ArrayLike<number>, props?: PrismProps): Primitive {
|
||||
const sideCount = points.length / 3;
|
||||
if (sideCount < 4) throw new Error('need at least 4 points to build a prism');
|
||||
if (sideCount < 3) throw new Error('need at least 3 points to build a prism');
|
||||
|
||||
const { height, topCap, bottomCap } = { ...DefaultPrismProps, ...props };
|
||||
|
||||
const count = 4 * sideCount;
|
||||
const builder = PrimitiveBuilder(count);
|
||||
let triangleCount = sideCount * 2;
|
||||
let vertexCount = sideCount * 4;
|
||||
|
||||
const capCount = (topCap ? 1 : 0) + (bottomCap ? 1 : 0);
|
||||
if (sideCount === 3) {
|
||||
triangleCount += capCount;
|
||||
vertexCount += capCount * 3;
|
||||
} else if (sideCount === 4) {
|
||||
triangleCount += capCount * 2;
|
||||
vertexCount += capCount * 4;
|
||||
} else {
|
||||
triangleCount += capCount * sideCount;
|
||||
vertexCount += capCount * sideCount * 3;
|
||||
}
|
||||
|
||||
const builder = PrimitiveBuilder(triangleCount, vertexCount);
|
||||
const halfHeight = height * 0.5;
|
||||
|
||||
Vec3.set(on, 0, 0, -halfHeight);
|
||||
@@ -42,22 +56,51 @@ export function Prism(points: ArrayLike<number>, props?: PrismProps): Primitive
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -halfHeight);
|
||||
Vec3.set(c, points[ni * 3], points[ni * 3 + 1], halfHeight);
|
||||
Vec3.set(d, points[i * 3], points[i * 3 + 1], halfHeight);
|
||||
builder.add(a, b, c);
|
||||
builder.add(c, d, a);
|
||||
builder.addQuad(a, b, c, d);
|
||||
}
|
||||
|
||||
// create bases
|
||||
for (let i = 0; i < sideCount; ++i) {
|
||||
const ni = (i + 1) % sideCount;
|
||||
if (sideCount === 3) {
|
||||
if (topCap) {
|
||||
Vec3.set(a, points[i * 3], points[i * 3 + 1], -halfHeight);
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -halfHeight);
|
||||
builder.add(on, b, a);
|
||||
Vec3.set(a, points[0], points[1], -halfHeight);
|
||||
Vec3.set(b, points[3], points[4], -halfHeight);
|
||||
Vec3.set(c, points[6], points[7], -halfHeight);
|
||||
builder.add(c, b, a);
|
||||
}
|
||||
if (bottomCap) {
|
||||
Vec3.set(a, points[i * 3], points[i * 3 + 1], halfHeight);
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], halfHeight);
|
||||
builder.add(a, b, op);
|
||||
Vec3.set(a, points[0], points[1], halfHeight);
|
||||
Vec3.set(b, points[3], points[4], halfHeight);
|
||||
Vec3.set(c, points[6], points[7], halfHeight);
|
||||
builder.add(a, b, c);
|
||||
}
|
||||
} else if (sideCount === 4) {
|
||||
if (topCap) {
|
||||
Vec3.set(a, points[0], points[1], -halfHeight);
|
||||
Vec3.set(b, points[3], points[4], -halfHeight);
|
||||
Vec3.set(c, points[6], points[7], -halfHeight);
|
||||
Vec3.set(d, points[9], points[10], -halfHeight);
|
||||
builder.addQuad(d, c, b, a);
|
||||
}
|
||||
if (bottomCap) {
|
||||
Vec3.set(a, points[0], points[1], halfHeight);
|
||||
Vec3.set(b, points[3], points[4], halfHeight);
|
||||
Vec3.set(c, points[6], points[7], halfHeight);
|
||||
Vec3.set(d, points[9], points[10], halfHeight);
|
||||
builder.addQuad(a, b, c, d);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < sideCount; ++i) {
|
||||
const ni = (i + 1) % sideCount;
|
||||
if (topCap) {
|
||||
Vec3.set(a, points[i * 3], points[i * 3 + 1], -halfHeight);
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -halfHeight);
|
||||
builder.add(on, b, a);
|
||||
}
|
||||
if (bottomCap) {
|
||||
Vec3.set(a, points[i * 3], points[i * 3 + 1], halfHeight);
|
||||
Vec3.set(b, points[ni * 3], points[ni * 3 + 1], halfHeight);
|
||||
builder.add(a, b, op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,7 +10,7 @@ import { polygon } from './polygon';
|
||||
import { Cage } from './cage';
|
||||
|
||||
const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5);
|
||||
const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero();
|
||||
const a = Vec3(), b = Vec3(), c = Vec3(), d = Vec3();
|
||||
|
||||
/**
|
||||
* Create a pyramid with a polygonal base
|
||||
@@ -18,8 +18,9 @@ const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero();
|
||||
export function Pyramid(points: ArrayLike<number>): Primitive {
|
||||
const sideCount = points.length / 3;
|
||||
const baseCount = sideCount === 3 ? 1 : sideCount === 4 ? 2 : sideCount;
|
||||
const count = 2 * baseCount + 2 * sideCount;
|
||||
const builder = PrimitiveBuilder(count);
|
||||
const triangleCount = baseCount + sideCount;
|
||||
const vertexCount = sideCount === 4 ? (sideCount * 3 + 4) : triangleCount * 3;
|
||||
const builder = PrimitiveBuilder(triangleCount, vertexCount);
|
||||
|
||||
// create sides
|
||||
for (let i = 0; i < sideCount; ++i) {
|
||||
@@ -40,8 +41,7 @@ export function Pyramid(points: ArrayLike<number>): Primitive {
|
||||
Vec3.set(b, points[3], points[4], -0.5);
|
||||
Vec3.set(c, points[6], points[7], -0.5);
|
||||
Vec3.set(d, points[9], points[10], -0.5);
|
||||
builder.add(c, b, a);
|
||||
builder.add(a, d, c);
|
||||
builder.addQuad(d, c, b, a);
|
||||
} else {
|
||||
for (let i = 0; i < sideCount; ++i) {
|
||||
const ni = (i + 1) % sideCount;
|
||||
|
||||
@@ -21,7 +21,7 @@ export function normalizeVec3Array<T extends NumberArray> (a: T, count: number)
|
||||
return a;
|
||||
}
|
||||
|
||||
const tmpV3 = Vec3.zero();
|
||||
const tmpV3 = Vec3();
|
||||
|
||||
export function transformPositionArray (t: Mat4, array: NumberArray, offset: number, count: number) {
|
||||
for (let i = 0, il = count * 3; i < il; i += 3) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Iterator } from '../../mol-data';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { NullLocation, Location } from '../../mol-model/location';
|
||||
|
||||
export interface LocationValue {
|
||||
@@ -15,31 +16,28 @@ export interface LocationValue {
|
||||
isSecondary: boolean
|
||||
}
|
||||
|
||||
export const NullLocationValue: LocationValue = {
|
||||
location: NullLocation,
|
||||
index: 0,
|
||||
groupIndex: 0,
|
||||
instanceIndex: 0,
|
||||
isSecondary: false
|
||||
};
|
||||
|
||||
export interface LocationIterator extends Iterator<LocationValue> {
|
||||
readonly hasNext: boolean
|
||||
readonly isNextNewInstance: boolean
|
||||
readonly groupCount: number
|
||||
readonly instanceCount: number
|
||||
readonly count: number
|
||||
/** If true, may have multiple units per instance; if false one unit per instance */
|
||||
readonly isComplex: boolean
|
||||
readonly stride: number
|
||||
readonly nonInstanceable: boolean
|
||||
move(): LocationValue
|
||||
reset(): void
|
||||
skipInstance(): void
|
||||
voidInstances(): void
|
||||
}
|
||||
|
||||
type LocationGetter = (groupIndex: number, instanceIndex: number) => Location
|
||||
type IsSecondaryGetter = (groupIndex: number, instanceIndex: number) => boolean
|
||||
|
||||
export function LocationIterator(groupCount: number, instanceCount: number, getLocation: LocationGetter, isComplex = false, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
|
||||
export function LocationIterator(groupCount: number, instanceCount: number, stride: number, getLocation: LocationGetter, nonInstanceable = false, isSecondary: IsSecondaryGetter = () => false): LocationIterator {
|
||||
if (groupCount % stride !== 0) {
|
||||
throw new Error('incompatible groupCount and stride');
|
||||
}
|
||||
|
||||
const value: LocationValue = {
|
||||
location: NullLocation as Location,
|
||||
index: 0,
|
||||
@@ -52,6 +50,7 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
|
||||
let isNextNewInstance = false;
|
||||
let groupIndex = 0;
|
||||
let instanceIndex = 0;
|
||||
let voidInstances = false;
|
||||
|
||||
return {
|
||||
get hasNext () { return hasNext; },
|
||||
@@ -59,15 +58,16 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
|
||||
groupCount,
|
||||
instanceCount,
|
||||
count: groupCount * instanceCount,
|
||||
isComplex,
|
||||
stride,
|
||||
nonInstanceable,
|
||||
move() {
|
||||
if (hasNext) {
|
||||
value.groupIndex = groupIndex;
|
||||
value.instanceIndex = instanceIndex;
|
||||
value.index = instanceIndex * groupCount + groupIndex;
|
||||
value.location = getLocation(groupIndex, instanceIndex);
|
||||
value.isSecondary = isSecondary(groupIndex, instanceIndex);
|
||||
++groupIndex;
|
||||
value.location = getLocation(groupIndex, voidInstances ? -1 : instanceIndex);
|
||||
value.isSecondary = isSecondary(groupIndex, voidInstances ? -1 : instanceIndex);
|
||||
groupIndex += stride;
|
||||
if (groupIndex === groupCount) {
|
||||
++instanceIndex;
|
||||
isNextNewInstance = true;
|
||||
@@ -90,6 +90,7 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
|
||||
isNextNewInstance = false;
|
||||
groupIndex = 0;
|
||||
instanceIndex = 0;
|
||||
voidInstances = false;
|
||||
},
|
||||
skipInstance() {
|
||||
if (hasNext && value.instanceIndex === instanceIndex) {
|
||||
@@ -97,6 +98,23 @@ export function LocationIterator(groupCount: number, instanceCount: number, getL
|
||||
groupIndex = 0;
|
||||
hasNext = instanceIndex < instanceCount;
|
||||
}
|
||||
},
|
||||
voidInstances() {
|
||||
voidInstances = true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/** A position Location */
|
||||
export interface PositionLocation {
|
||||
readonly kind: 'position-location',
|
||||
readonly position: Vec3
|
||||
}
|
||||
export function PositionLocation(position?: Vec3): PositionLocation {
|
||||
return { kind: 'position-location', position: position ? Vec3.clone(position) : Vec3() };
|
||||
}
|
||||
export function isPositionLocation(x: any): x is PositionLocation {
|
||||
return !!x && x.kind === 'position-location';
|
||||
}
|
||||
@@ -145,7 +145,7 @@ class MarchingCubesState {
|
||||
// clear either the top or bottom half of the buffer...
|
||||
const start = k % 2 === 0 ? 0 : 3 * this.nX * this.nY;
|
||||
const end = k % 2 === 0 ? 3 * this.nX * this.nY : this.verticesOnEdges.length;
|
||||
for (let i = start; i < end; i++) this.verticesOnEdges[i] = 0;
|
||||
this.verticesOnEdges.fill(0, start, end);
|
||||
}
|
||||
|
||||
private interpolate(edgeNum: number) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user