mirror of
https://github.com/molstar/molstar.git
synced 2026-06-06 22:54:22 +08:00
Compare commits
374 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a3b54ff88c | ||
|
|
3e7614d75c | ||
|
|
a01e8f26bd | ||
|
|
351faf3c45 | ||
|
|
8f20571a17 | ||
|
|
c25a4247e6 | ||
|
|
1071d3d8ba | ||
|
|
e8dc046570 | ||
|
|
27f9c2aa67 | ||
|
|
a4962231c8 | ||
|
|
8833f29ce5 | ||
|
|
40b6038380 | ||
|
|
59e16e0187 | ||
|
|
57a790544c | ||
|
|
df0669598c | ||
|
|
9efb5cd126 | ||
|
|
08a56ad6ab | ||
|
|
f500372c16 | ||
|
|
d7ad5a6e9f | ||
|
|
2714d32e15 | ||
|
|
86a74d1cc2 | ||
|
|
3f0f24cb99 | ||
|
|
b8ddc142ea | ||
|
|
cccaa48589 | ||
|
|
3ad355ad40 | ||
|
|
918186eb24 | ||
|
|
db4742cebf | ||
|
|
7d6c77b3bd | ||
|
|
dfcc4e400d | ||
|
|
2d400d9166 | ||
|
|
ebceecb3e6 | ||
|
|
a87f92bf7d | ||
|
|
4033bc93c2 | ||
|
|
6c4ba7af61 | ||
|
|
bc9584e49b | ||
|
|
c9734d83a2 | ||
|
|
93943cc27b | ||
|
|
25836b2de0 | ||
|
|
c6874c922d | ||
|
|
550d898c4f | ||
|
|
5e16c340dc | ||
|
|
bcb18a8faf | ||
|
|
cd7d8f704e | ||
|
|
0d197b2dc5 | ||
|
|
b3ce268f0e | ||
|
|
2da02daadc | ||
|
|
999e5a47af | ||
|
|
ff3fad0789 | ||
|
|
5af6265c07 | ||
|
|
4ad7a96191 | ||
|
|
71215d183d | ||
|
|
e864f13a66 | ||
|
|
59298be573 | ||
|
|
2a57867167 | ||
|
|
a817a70b46 | ||
|
|
5afb7981ae | ||
|
|
2308dfd22e | ||
|
|
f0de2cea2c | ||
|
|
8f9c687935 | ||
|
|
1f56405d79 | ||
|
|
37af8c66a1 | ||
|
|
a1fefa2efa | ||
|
|
958f3011e4 | ||
|
|
8d48fa67ae | ||
|
|
87158db7c0 | ||
|
|
bac65cc71e | ||
|
|
3ece0c74c6 | ||
|
|
89fbb690fe | ||
|
|
918d02fec4 | ||
|
|
8af3a240b5 | ||
|
|
a80284ac02 | ||
|
|
1b48f4c32a | ||
|
|
1055eab4c5 | ||
|
|
d4445cef5c | ||
|
|
063d327a5f | ||
|
|
34f409e683 | ||
|
|
8415ed1b92 | ||
|
|
c92147289e | ||
|
|
5b16213cd2 | ||
|
|
bbd36e1838 | ||
|
|
1a0b30d6eb | ||
|
|
e309e8917a | ||
|
|
14135b8386 | ||
|
|
3230a6a7dc | ||
|
|
34ebc5ab7a | ||
|
|
d8b62c5cbb | ||
|
|
358ef44780 | ||
|
|
fb2f79a395 | ||
|
|
e3a95e0a08 | ||
|
|
a1b09ccc1c | ||
|
|
26b31b3fcc | ||
|
|
3027418d31 | ||
|
|
5a69fb691d | ||
|
|
cced98c93f | ||
|
|
6c299161fe | ||
|
|
10575ac361 | ||
|
|
d715330d8e | ||
|
|
fdba049982 | ||
|
|
270d7386b2 | ||
|
|
a28c2f0995 | ||
|
|
196e17ff0d | ||
|
|
c7efac0a78 | ||
|
|
75eb04070c | ||
|
|
d6c9ae1fbe | ||
|
|
24a6403025 | ||
|
|
842e5d890e | ||
|
|
b34d1cca00 | ||
|
|
af4dc090c4 | ||
|
|
1fa090d162 | ||
|
|
4d5b749e3e | ||
|
|
6a736eb89f | ||
|
|
cfead0481f | ||
|
|
fbbd7e623e | ||
|
|
f16707b849 | ||
|
|
76b0b23c07 | ||
|
|
d4a2bd7cba | ||
|
|
c572feb1d2 | ||
|
|
121f8eab3e | ||
|
|
8c49b82c3d | ||
|
|
ac7faf8524 | ||
|
|
5d6d91a331 | ||
|
|
d476db556d | ||
|
|
354d092834 | ||
|
|
5cde26a8e2 | ||
|
|
b905178395 | ||
|
|
543d014d0d | ||
|
|
a17afa59b3 | ||
|
|
bd6be354d5 | ||
|
|
2058d605c7 | ||
|
|
ba60188758 | ||
|
|
52f2ddf715 | ||
|
|
aa20fffbfb | ||
|
|
b59d11c91a | ||
|
|
8fdc29d048 | ||
|
|
d52ea41051 | ||
|
|
852be261dd | ||
|
|
95e9a3012d | ||
|
|
dd0a45c154 | ||
|
|
4692d63a2b | ||
|
|
0689ecabb6 | ||
|
|
424f576e99 | ||
|
|
4407994195 | ||
|
|
4c6331e72d | ||
|
|
825514dd10 | ||
|
|
8e2967b993 | ||
|
|
a46ba63e31 | ||
|
|
cf9fe99a81 | ||
|
|
2909a209c3 | ||
|
|
1322882444 | ||
|
|
119c548fa7 | ||
|
|
539442f710 | ||
|
|
8841f04af6 | ||
|
|
8c2d3a577a | ||
|
|
bed4b728d3 | ||
|
|
aa87acc0a7 | ||
|
|
7d1e2b44db | ||
|
|
c1c1badf62 | ||
|
|
0937c84f47 | ||
|
|
6a7f892d60 | ||
|
|
b4cd2d0a11 | ||
|
|
2067f02830 | ||
|
|
6d86ada6b4 | ||
|
|
f656cf09b7 | ||
|
|
a891b4c551 | ||
|
|
ded844c936 | ||
|
|
44b36637fd | ||
|
|
f590bd0f0a | ||
|
|
9474c80673 | ||
|
|
7b48d691c8 | ||
|
|
b03146852f | ||
|
|
e270a83909 | ||
|
|
a40b737c6f | ||
|
|
942533ed2b | ||
|
|
546f3cd3c5 | ||
|
|
21597b1fdd | ||
|
|
31d8568c1a | ||
|
|
3630cd14e8 | ||
|
|
4f083f10e6 | ||
|
|
371ef984c0 | ||
|
|
e2db1257cd | ||
|
|
c812e72a1a | ||
|
|
ef9b89820d | ||
|
|
9345f3584a | ||
|
|
4d058aa1a8 | ||
|
|
e7da6092aa | ||
|
|
94f6b864b0 | ||
|
|
6e90447511 | ||
|
|
b91030c4bd | ||
|
|
5fd453c77a | ||
|
|
7f2b10674e | ||
|
|
238e5e0b88 | ||
|
|
1f26b5c339 | ||
|
|
eac478e7cb | ||
|
|
d0c59fdc92 | ||
|
|
7e61bcad32 | ||
|
|
7e98870dce | ||
|
|
405dc0d90a | ||
|
|
7a362c816e | ||
|
|
7e1396b74c | ||
|
|
68ad1ec065 | ||
|
|
430f8da44e | ||
|
|
68866cd2de | ||
|
|
05888bec50 | ||
|
|
65e1cb4a5d | ||
|
|
50f571b0d3 | ||
|
|
d86b31edf8 | ||
|
|
ec107352b4 | ||
|
|
1d42d5a2d6 | ||
|
|
02d1dcb9d9 | ||
|
|
d86c3621b7 | ||
|
|
f2724491c2 | ||
|
|
f4c84a6930 | ||
|
|
0eb9b286b4 | ||
|
|
da006391da | ||
|
|
130e33f8c3 | ||
|
|
109e528d1c | ||
|
|
5df69cd84a | ||
|
|
973afa2237 | ||
|
|
0088d3e1bf | ||
|
|
26e6a11fa8 | ||
|
|
056e2c5182 | ||
|
|
0e7cde24bc | ||
|
|
36ce262970 | ||
|
|
289c8181c8 | ||
|
|
eb0fd490d4 | ||
|
|
70c073c43c | ||
|
|
0565df4df9 | ||
|
|
6dd425cb55 | ||
|
|
c0f994a506 | ||
|
|
63705ed158 | ||
|
|
fda9069f17 | ||
|
|
66bffd8403 | ||
|
|
4a7d83c85b | ||
|
|
fef649ce09 | ||
|
|
794f81bb8e | ||
|
|
bc5648620d | ||
|
|
07897f57f3 | ||
|
|
77f756dfe0 | ||
|
|
15eef7b688 | ||
|
|
5d0ba7504b | ||
|
|
8d59b5b814 | ||
|
|
86871124d5 | ||
|
|
1a328d98b6 | ||
|
|
fcbc3ab3d0 | ||
|
|
f36093dad9 | ||
|
|
d4c2bb85cb | ||
|
|
d53c1e8e65 | ||
|
|
f189d0bdab | ||
|
|
395eddd927 | ||
|
|
eb1d48a73c | ||
|
|
38b0bb8d7d | ||
|
|
0daffa6b57 | ||
|
|
e1d5d369f1 | ||
|
|
6b88acd2bc | ||
|
|
6384eac7e7 | ||
|
|
fd409ce27f | ||
|
|
c21c9f5160 | ||
|
|
71d00a22dd | ||
|
|
5554697028 | ||
|
|
709ac8430a | ||
|
|
2ee08f6161 | ||
|
|
3608578528 | ||
|
|
5114a211fd | ||
|
|
8cc947c998 | ||
|
|
9105737834 | ||
|
|
84bcbd1ca6 | ||
|
|
144967dbd3 | ||
|
|
bdb33b9398 | ||
|
|
ade6ef5631 | ||
|
|
3bc60d1d59 | ||
|
|
4068c45eb4 | ||
|
|
52d0ff4a67 | ||
|
|
f346d15bef | ||
|
|
b2ce1fc6fa | ||
|
|
4e331001ef | ||
|
|
7d67304e4c | ||
|
|
babda601cb | ||
|
|
8bdfff5e94 | ||
|
|
be4b408ddc | ||
|
|
230697fbb4 | ||
|
|
78ab6b0c95 | ||
|
|
31819dbf16 | ||
|
|
1f0c24b58e | ||
|
|
f5c587bfe5 | ||
|
|
52b1c7e4d9 | ||
|
|
e76d02bc8c | ||
|
|
481b763049 | ||
|
|
7bfef2ae40 | ||
|
|
01fe10ebdc | ||
|
|
1665dd7d00 | ||
|
|
4e4b80a7b2 | ||
|
|
9716fecdb9 | ||
|
|
684fd2d237 | ||
|
|
9432b9a7a7 | ||
|
|
3a37c95c17 | ||
|
|
6040b99c19 | ||
|
|
83bef0f0e7 | ||
|
|
95bb3a1f81 | ||
|
|
be677f47cb | ||
|
|
43bf69d09c | ||
|
|
b6cc626431 | ||
|
|
931fdfca9b | ||
|
|
1c10db5656 | ||
|
|
c4ccd8758f | ||
|
|
6c99c575bc | ||
|
|
ae2493b6e3 | ||
|
|
bcd50c294f | ||
|
|
9c0024dbab | ||
|
|
c15b3603c0 | ||
|
|
70647ba972 | ||
|
|
8d19357845 | ||
|
|
8e9817c4d1 | ||
|
|
b16147b88c | ||
|
|
9840d8f816 | ||
|
|
d892ccab4c | ||
|
|
65f88b3293 | ||
|
|
9e6e5eb795 | ||
|
|
2f755efeec | ||
|
|
012e616ec4 | ||
|
|
007d0e7608 | ||
|
|
bf313073b9 | ||
|
|
293928f3de | ||
|
|
2404f398b6 | ||
|
|
43ff6e24c8 | ||
|
|
9e62112366 | ||
|
|
026d6fc618 | ||
|
|
95fcd942dc | ||
|
|
805481db14 | ||
|
|
39175df025 | ||
|
|
cd0f451f6b | ||
|
|
fe1aa1a9bf | ||
|
|
fcfb6e6d5a | ||
|
|
c548c94575 | ||
|
|
2d45f4a77c | ||
|
|
a5ae887842 | ||
|
|
e4b53cdc6a | ||
|
|
c53940e67e | ||
|
|
6d61745f0f | ||
|
|
46d86d93b0 | ||
|
|
11772b64fb | ||
|
|
dbc8ab00c6 | ||
|
|
015fad4371 | ||
|
|
71a484586f | ||
|
|
f0b06ee746 | ||
|
|
b0694b886b | ||
|
|
eaf47b3169 | ||
|
|
ad9046fcf2 | ||
|
|
eabe4d46bc | ||
|
|
003c5f8fb7 | ||
|
|
68748a4a94 | ||
|
|
9bd6b8195d | ||
|
|
05848b651c | ||
|
|
0a8f87dd9f | ||
|
|
925aaa701d | ||
|
|
5be599bad4 | ||
|
|
e22ce53e65 | ||
|
|
4c49431027 | ||
|
|
4192d82ef3 | ||
|
|
ce220737f2 | ||
|
|
eeb7cd2c52 | ||
|
|
748111beb2 | ||
|
|
1f7d41c653 | ||
|
|
b9430ff387 | ||
|
|
6591bab035 | ||
|
|
4da446aec2 | ||
|
|
25c170e36d | ||
|
|
eba18d1dce | ||
|
|
2c87d01a5e | ||
|
|
e41a2baa32 | ||
|
|
c297017749 | ||
|
|
9a0fc1faa6 | ||
|
|
424513f23c | ||
|
|
895d672589 | ||
|
|
0c6253ed16 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -4,6 +4,54 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
- Fix empty transforms default in `ShapeFromPly`
|
||||
- Add `Camera.changed` event and rotation/translation setter/getter
|
||||
- Add `instanceGranularity: 'auto'` as a memory guard
|
||||
- Honor `instanceGranularity` in `Visual.getLoci`
|
||||
- Add mesoscale representation preset
|
||||
- Add presets option to `ObjectList` param definition
|
||||
- Fix memory leak in `State.dispose()` not invoking transformer `dispose` callbacks for live cells
|
||||
- Adds File/Open and drag-and-drop support for Kinemage files in the viewer app
|
||||
|
||||
## [v5.9.0] - 2026-05-03
|
||||
- Fix edge case when `PluginSpec.animations` is empty
|
||||
- Add 8K UHD option to `ViewportScreenshotHelper`
|
||||
- Handle MRC files with empty length header fields
|
||||
- Handle CCD bonds with Deuterium atoms
|
||||
- [Breaking] ComponentBond.Entry.map now returns ComponentBond.Pairs
|
||||
- Fix volume slice marking performance regression
|
||||
- Add GPU procedural animation (wiggle & tumble)
|
||||
- Per-vertex wiggle via fbm noise (position & group mode)
|
||||
- Per-instance tumble via fbm noise (rotation + translation)
|
||||
- `Wiggle` theme layer for data-driven per-group wiggle
|
||||
- `enableAnimation` Canvas3D param for global toggle
|
||||
- Add `AnimateTime` built-in for, e.g., exporting procedural animation
|
||||
- Add Procedural Animation panels
|
||||
- Viewer: structure dynamics & uncertainty
|
||||
- Mesoscale Explorer: entity dynamics
|
||||
- Fix `GraphQLClient` missing required headers
|
||||
- [Breaking] Use Record instead of Array for headers (assets & data-source utils)
|
||||
|
||||
## [v5.8.0] - 2026-04-03
|
||||
- Dependencies: remove `utils.promisify`, `node-fetch` (#1797)
|
||||
- Fix circular dependency which causes crash in bundlers (#1791)
|
||||
- Add `putty` as a mol-view-spec representation.
|
||||
- Fix detecting sidechain-only structures as coarse-grained (#1420)
|
||||
- Fix clip-object transform due to missing axis normalization
|
||||
- Sequence alignment: Fix return type & improve scoring for unknown residues
|
||||
- Use PDB SEQRES block to show unresolved residues in Sequence toolbar
|
||||
- Canvas3D debug-helpers
|
||||
- [Breaking] Move helpers to an extension as a PluginBehavior (params are no longer part of Canvas3D)
|
||||
- Add helpers for clip-object, direct-volume, image, mesh
|
||||
- Fix StructureComponent node update throwing error when substructure empty
|
||||
- CSS: Avoid tooltip box flickering when hovering something under it
|
||||
- Volume slice visual
|
||||
- Fix support for volume instances
|
||||
- Fix plane mode: ensure normalized & correctly oriented
|
||||
- MolViewSpec
|
||||
- Add `VolumeStreamingExtension` (`molstar_volume_streaming` custom property)
|
||||
- Fix focusing empty selections
|
||||
- Avoid re-calculating static model properties for trajectories
|
||||
|
||||
## [v5.7.0] - 2026-02-28
|
||||
- Text label improvements
|
||||
|
||||
@@ -48,4 +48,7 @@
|
||||
* CLR (e.g. 3GKI) - four fused rings
|
||||
* Assembly symmetries
|
||||
* 5M30 (Assembly 1, C3 local and pseudo)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* 1RB8 (Assembly 1, I global)
|
||||
* Deuterium atoms
|
||||
* 3CWH (XUL with D and DOD)
|
||||
* 8TT8 (HOH and other with D)
|
||||
3914
package-lock.json
generated
3914
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
45
package.json
45
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "5.7.0",
|
||||
"version": "5.9.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -74,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
"\\.ts$": ["esbuild-jest-transform", { "tsconfigRaw": "{\"compilerOptions\":{\"useDefineForClassFields\":false}}" }]
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
@@ -124,7 +124,9 @@
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>",
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>"
|
||||
"Diego del Alamo <diego.delalamo@gmail.com>",
|
||||
"Tianzhen Lin (Tangent) <tangent@usa.net>",
|
||||
"Russ Taylor <russ@reliasolve.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
@@ -135,51 +137,48 @@
|
||||
"@types/react": "^18.3.28",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.59.1",
|
||||
"@typescript-eslint/parser": "^8.59.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"cpx2": "^8.0.0",
|
||||
"cpx2": "^8.0.2",
|
||||
"css-loader": "^7.1.4",
|
||||
"esbuild": "^0.27.3",
|
||||
"esbuild": "^0.28.0",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.6.0",
|
||||
"eslint": "^10.0.2",
|
||||
"fs-extra": "^11.3.3",
|
||||
"globals": "^17.3.0",
|
||||
"esbuild-sass-plugin": "^3.7.0",
|
||||
"eslint": "^10.3.0",
|
||||
"fs-extra": "^11.3.4",
|
||||
"globals": "^17.6.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^30.2.0",
|
||||
"jest": "^30.3.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.97.3",
|
||||
"simple-git": "^3.32.3",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
"sass": "^1.99.0",
|
||||
"simple-git": "^3.36.0",
|
||||
"tsc-alias": "^1.8.17",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^22.19.13",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/node": "^22.19.17",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immutable": "^5.1.4",
|
||||
"immutable": "^5.1.5",
|
||||
"io-ts": "^2.2.22",
|
||||
"mutative": "^1.3.0",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.32.0",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.3"
|
||||
"swagger-ui-dist": "^5.32.5",
|
||||
"tslib": "^2.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -55,7 +55,7 @@ function getSpacefillParams(color: Color, sizeFactor: number, graphics: Graphics
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: 1,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../../../mol-model/structure/model/model';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
@@ -59,7 +60,7 @@ function getSpacefillParams(color: Color, scaleFactor: number, graphics: Graphic
|
||||
sizeTheme: {
|
||||
name: 'physical',
|
||||
params: {
|
||||
value: 1,
|
||||
scale: scaleFactor,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -102,6 +103,8 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
});
|
||||
}
|
||||
|
||||
const coarseGrained = Model.isCoarseGrained(model.data!);
|
||||
|
||||
const entGroups = new Map<string, StateObjectSelector>();
|
||||
const entIds = new Map<string, { idx: number, members: Map<number, number> }>();
|
||||
const entColors = new Map<string, Color[]>();
|
||||
@@ -170,7 +173,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
for (let i = 0; i < entities._rowCount; i++) {
|
||||
const t = getEntityType(i);
|
||||
const color = entColors.get(t)![entIds.get(t)!.members.get(i)!];
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || 1;
|
||||
const scaleFactor = spheresAvgRadius.get(entities.id.value(i)) || (coarseGrained ? 2 : 1);
|
||||
|
||||
build = build
|
||||
.toRoot()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -10,6 +10,7 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
import { getAnimationParam } from '../../../mol-geo/geometry/animation';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
@@ -21,7 +22,6 @@ import { Hcl } from '../../../mol-util/color/spaces/hcl';
|
||||
import { StateObjectCell, StateObjectRef, StateSelection } from '../../../mol-state';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
|
||||
import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/representation/spacefill';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { saturate } from '../../../mol-math/interpolate';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
@@ -174,6 +174,8 @@ export const LodParams = {
|
||||
approximate: Spheres.Params.approximate,
|
||||
};
|
||||
|
||||
export const AnimationParams = getAnimationParam().params;
|
||||
|
||||
export const SimpleClipParams = {
|
||||
type: PD.Select('none', PD.objectToOptions(Clip.Type, t => stringToWords(t))),
|
||||
invert: PD.Boolean(false),
|
||||
@@ -281,6 +283,7 @@ export const MesoscaleGroupParams = {
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
lod: PD.Group(LodParams),
|
||||
clip: PD.Group(SimpleClipParams),
|
||||
animation: PD.Group(AnimationParams),
|
||||
};
|
||||
export type MesoscaleGroupProps = PD.Values<typeof MesoscaleGroupParams>;
|
||||
|
||||
@@ -318,38 +321,7 @@ export function getMesoscaleGroupParams(graphicsMode: GraphicsMode): MesoscaleGr
|
||||
export type LodLevels = typeof SpacefillRepresentationProvider.defaultValues['lodLevels']
|
||||
|
||||
export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): LodLevels {
|
||||
switch (graphicsMode) {
|
||||
case 'performance':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
];
|
||||
case 'balanced':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
];
|
||||
case 'quality':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
];
|
||||
case 'ultra':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
];
|
||||
default:
|
||||
assertUnreachable(graphicsMode);
|
||||
}
|
||||
return Spheres.LodLevelsPresets[graphicsMode];
|
||||
}
|
||||
|
||||
export type GraphicsMode = 'ultra' | 'quality' | 'balanced' | 'performance' | 'custom';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { EveryLoci, Loci } from '../../../mol-model/loci';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, AnimationParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import React, { useState } from 'react';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { StructureElement } from '../../../mol-model/structure/structure/element';
|
||||
@@ -828,6 +828,26 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
update.commit();
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
for (const r of this.allFilteredEntities) {
|
||||
update.to(r).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = values;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const g of this.allGroups) {
|
||||
update.to(g).update(old => {
|
||||
old.animation = values;
|
||||
});
|
||||
}
|
||||
|
||||
update.commit();
|
||||
};
|
||||
|
||||
update = (props: MesoscaleGroupProps) => {
|
||||
this.plugin.state.data.build().to(this.ref).update(props);
|
||||
};
|
||||
@@ -865,6 +885,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const rootValue = this.cell.params?.values.color;
|
||||
const clipValue = this.cell.params?.values.clip;
|
||||
const lodValue = this.cell.params?.values.lod;
|
||||
const animationValue = this.cell.params?.values.animation;
|
||||
const isRoot = this.cell.params?.values.root;
|
||||
|
||||
const groups = this.groups;
|
||||
@@ -904,6 +925,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterControls params={SimpleClipParams} values={clipValue} onChangeValues={this.updateClip} />
|
||||
<ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />
|
||||
<ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
{this.state.action === 'root' && <div style={{ marginRight: 5 }} className='msp-accent-offset'>
|
||||
@@ -1080,6 +1102,19 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
};
|
||||
}
|
||||
|
||||
get animationValue(): PD.Values<typeof AnimationParams> | undefined {
|
||||
const p = this.cell.transform.params?.type?.params?.animation;
|
||||
if (!p) return;
|
||||
return {
|
||||
wiggleMode: p.wiggleMode,
|
||||
wiggleSpeed: p.wiggleSpeed,
|
||||
wiggleAmplitude: p.wiggleAmplitude,
|
||||
wiggleFrequency: p.wiggleFrequency,
|
||||
tumbleSpeed: p.tumbleSpeed,
|
||||
tumbleAmplitude: p.tumbleAmplitude,
|
||||
};
|
||||
}
|
||||
|
||||
get patternValue(): { amplitude: number, frequency: number } | undefined {
|
||||
const p = this.cell.transform.params;
|
||||
if (p.type) return;
|
||||
@@ -1194,6 +1229,15 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
}
|
||||
};
|
||||
|
||||
updateAnimation = (values: PD.Values) => {
|
||||
const params = this.cell.transform.params as StateTransformer.Params<StructureRepresentation3D>;
|
||||
if (!params.type) return;
|
||||
|
||||
this.plugin.build().to(this.ref).update(old => {
|
||||
old.type.params.animation = values;
|
||||
}).commit();
|
||||
};
|
||||
|
||||
updatePattern = (values: PD.Values) => {
|
||||
return this.plugin.build().to(this.ref).update(old => {
|
||||
if (!old.type) {
|
||||
@@ -1213,6 +1257,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
const opacityValue = this.opacityValue;
|
||||
const emissiveValue = this.emissiveValue;
|
||||
const lodValue = this.lodValue;
|
||||
const animationValue = this.animationValue;
|
||||
const patternValue = this.patternValue;
|
||||
|
||||
const l = getEntityLabel(this.plugin, this.cell);
|
||||
@@ -1251,6 +1296,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<ParameterMappingControl mapping={this.clipMapping} />
|
||||
{lodValue && <ParameterControls params={LodParams} values={lodValue} onChangeValues={this.updateLod} />}
|
||||
{animationValue && <ParameterControls params={AnimationParams} values={animationValue} onChangeValues={this.updateAnimation} />}
|
||||
</ControlGroup>
|
||||
</div>}
|
||||
</>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -13,7 +13,7 @@ import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { MesoscaleState } from '../data/state';
|
||||
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, MesoProceduralAnimationControls, ExplorerInfo } from './states';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
@@ -145,6 +145,7 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
<StructureMeasurementsControls initiallyCollapsed={true}/>
|
||||
</>
|
||||
<MesoQuickStylesControls />
|
||||
<MesoProceduralAnimationControls />
|
||||
<Spacer />
|
||||
<SectionHeader title='Entities' />
|
||||
<EntityControls />
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,7 +8,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { AnimationSvg, GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
|
||||
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
|
||||
@@ -24,7 +24,7 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { getAllEntities, getAllGroups, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
@@ -779,3 +779,110 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export class MesoProceduralAnimationControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
header: 'Procedural Animation',
|
||||
brand: { accent: 'gray' as const, svg: AnimationSvg }
|
||||
};
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
return <>
|
||||
<MesoProceduralAnimation />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
class MesoProceduralAnimation extends PluginUIComponent {
|
||||
private isMembrane(cell: { transform: { tags?: string[] } }) {
|
||||
return cell.transform.tags?.some(t => t.includes('mem')) ?? false;
|
||||
}
|
||||
|
||||
async dynamics() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
const membrane = this.isMembrane(entity);
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
const membrane = this.isMembrane(group);
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleMode: 'position',
|
||||
wiggleSpeed: 7,
|
||||
wiggleAmplitude: 1,
|
||||
wiggleFrequency: 0.2,
|
||||
tumbleSpeed: 1,
|
||||
tumbleAmplitude: membrane ? 0 : 4,
|
||||
tumbleFrequency: 0.2,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
async clear() {
|
||||
const update = this.plugin.state.data.build();
|
||||
const entities = getAllEntities(this.plugin);
|
||||
const groups = getAllGroups(this.plugin);
|
||||
|
||||
for (const entity of entities) {
|
||||
update.to(entity).update(old => {
|
||||
if (old.type) {
|
||||
old.type.params.animation = {
|
||||
...old.type.params.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const group of groups) {
|
||||
update.to(group).update(old => {
|
||||
old.animation = {
|
||||
...old.animation,
|
||||
wiggleAmplitude: 0,
|
||||
tumbleAmplitude: 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow title='Enable wiggle for all entities and tumble for non-membrane entities' onClick={() => this.dynamics()} style={{ width: 'auto' }}>
|
||||
Dynamics
|
||||
</Button>
|
||||
<Button noOverflow title='Set wiggle and tumble amplitude to zero for all entities' onClick={() => this.clear()} style={{ width: 'auto' }}>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { AssemblySymmetry } from '../../extensions/assembly-symmetry';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { DebugHelpers } from '../../extensions/debug-helpers';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat } from '../../extensions/g3d/format';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
@@ -27,16 +29,19 @@ import { ZenodoImport } from '../../extensions/zenodo';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import * as MVSUtil from '../../extensions/mvs/util';
|
||||
import { KinemageExtension } from '../../extensions/kinemage/behavior';
|
||||
|
||||
export const ExtensionMap = {
|
||||
// Mol* built-in extensions
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'debug-helpers': PluginSpec.Behavior(DebugHelpers),
|
||||
'model-export': PluginSpec.Behavior(ModelExport),
|
||||
'mp4-export': PluginSpec.Behavior(Mp4Export),
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'kinemage': PluginSpec.Behavior(KinemageExtension),
|
||||
|
||||
// 3rd party extensions
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Josh McMenemy <josh.mcmenemy@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -32,7 +31,7 @@ function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
|
||||
|
||||
function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated ion names params file. Names extracted from CCD components.
|
||||
*
|
||||
@@ -41,7 +40,7 @@ function writeIonNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as path from 'path';
|
||||
import util from 'util';
|
||||
import fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -44,7 +43,7 @@ function writeSaccharideNamesFile(filePath: string, ionNames: string[]) {
|
||||
|
||||
export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").replace(/,/g, ', ')});
|
||||
`;
|
||||
writeFile(filePath, output);
|
||||
writeFileAsync(filePath, output);
|
||||
}
|
||||
|
||||
async function run(out: string, options = DefaultDataOptions) {
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
require('util.promisify').shim();
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Database, Table, DatabaseCollection } from '../../mol-data/db';
|
||||
import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
|
||||
@@ -250,14 +249,14 @@ async function run(out: string, binary = false, options = DefaultDataOptions, cc
|
||||
if (!fs.existsSync(path.dirname(out))) {
|
||||
fs.mkdirSync(path.dirname(out));
|
||||
}
|
||||
writeFile(out, ccbCif);
|
||||
writeFileAsync(out, ccbCif);
|
||||
|
||||
if (!!ccaOut) {
|
||||
const ccaCif = getEncodedCif(CCA_TABLE_NAME, atoms, binary);
|
||||
if (!fs.existsSync(path.dirname(ccaOut))) {
|
||||
fs.mkdirSync(path.dirname(ccaOut));
|
||||
}
|
||||
writeFile(ccaOut, ccaCif);
|
||||
writeFileAsync(ccaOut, ccaCif);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
const readFile = util.promisify(fs.readFile);
|
||||
const writeFile = util.promisify(fs.writeFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
import { Progress } from '../../mol-task';
|
||||
import { Database } from '../../mol-data/db';
|
||||
@@ -27,9 +25,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
|
||||
fs.mkdirSync(DATA_DIR);
|
||||
}
|
||||
if (url.endsWith('.gz')) {
|
||||
await writeFile(path, zlib.gunzipSync(await data.buffer()));
|
||||
await writeFileAsync(path, zlib.gunzipSync(await data.arrayBuffer()));
|
||||
} else {
|
||||
await writeFile(path, await data.text());
|
||||
await writeFileAsync(path, await data.text());
|
||||
}
|
||||
console.log(`done downloading ${url}`);
|
||||
}
|
||||
@@ -41,7 +39,7 @@ export async function ensureDataAvailable(options: DataOptions) {
|
||||
}
|
||||
|
||||
export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
|
||||
const parsed = await parseCif(await readFile(path, 'utf8'));
|
||||
const parsed = await parseCif(await readFileAsync(path, 'utf8'));
|
||||
return CIF.toDatabaseCollection(schema, parsed.result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { CIF, CifCategory, getCifFieldType, CifField, CifFile } from '../../mol-io/reader/cif';
|
||||
@@ -22,7 +23,7 @@ function showProgress(p: Progress) {
|
||||
process.stdout.write(`\r${Progress.format(p)}`);
|
||||
}
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
const unzipAsync = util.promisify<zlib.InputType, Buffer>(zlib.unzip);
|
||||
|
||||
async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderResult<CifFile>> {
|
||||
|
||||
@@ -12,7 +12,6 @@ import * as fs from 'fs';
|
||||
import * as zlib from 'zlib';
|
||||
import { convert } from './converter';
|
||||
|
||||
require('util.promisify').shim();
|
||||
|
||||
async function process(srcPath: string, outPath: string, configPath?: string, filterPath?: string) {
|
||||
const config = configPath ? JSON.parse(fs.readFileSync(configPath, 'utf8')) : void 0;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
|
||||
import { parseCsv } from '../../mol-io/reader/csv/parser';
|
||||
import { CifFrame, CifBlock } from '../../mol-io/reader/cif';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -65,7 +65,9 @@ function getTypeDef(c: Column): string {
|
||||
case 'float': return 'float';
|
||||
case 'coord': return 'coord';
|
||||
case 'enum':
|
||||
return `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
return c.subType === 'int'
|
||||
? `Aliased<${c.values.join(' | ')}>(${c.subType})`
|
||||
: `Aliased<'${c.values.map(v => v.replace(/'/g, '\\\'')).join(`' | '`)}'>(${c.subType})`;
|
||||
case 'matrix':
|
||||
return `Matrix(${c.rows}, ${c.columns})`;
|
||||
case 'vector':
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import fetch from 'node-fetch';
|
||||
import { UniqueArray } from '../../mol-data/generic';
|
||||
|
||||
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as util from 'util';
|
||||
import * as fs from 'fs';
|
||||
import fetch from 'node-fetch';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CIF } from '../../mol-io/reader/cif';
|
||||
import { Progress } from '../../mol-task';
|
||||
|
||||
const readFileAsync = util.promisify(fs.readFile);
|
||||
const readFileAsync = fs.promises.readFile;
|
||||
|
||||
async function readFile(path: string) {
|
||||
if (path.match(/\.bcif$/)) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
*/
|
||||
|
||||
import * as argparse from 'argparse';
|
||||
require('util.promisify').shim();
|
||||
|
||||
import { CifFrame } from '../../mol-io/reader/cif';
|
||||
import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing, Trajectory } from '../../mol-model/structure';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as argparse from 'argparse';
|
||||
import * as util from 'util';
|
||||
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { downloadCif } from './helpers';
|
||||
@@ -20,8 +20,7 @@ import { createVolumeIsosurfaceMesh } from '../../mol-repr/volume/isosurface';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { volumeFromDensityServerData, DscifFormat } from '../../mol-model-formats/volume/density-server';
|
||||
|
||||
require('util.promisify').shim();
|
||||
const writeFileAsync = util.promisify(fs.writeFile);
|
||||
const writeFileAsync = fs.promises.writeFile;
|
||||
|
||||
async function getVolume(url: string): Promise<Volume> {
|
||||
const cif = await downloadCif(url, true);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import express from 'express';
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
async function getMappings(id: string) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import fetch from 'node-fetch';
|
||||
import { createMapping } from './mapping';
|
||||
|
||||
(async function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -118,6 +118,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
@@ -151,6 +152,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
_localPropertyData: Object.create(null)
|
||||
};
|
||||
|
||||
if (params.clampValues?.name === 'on') {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,32 +18,33 @@ import { TransformData } from '../../mol-geo/geometry/transform-data';
|
||||
import { sphereVertexCount } from '../../mol-geo/primitive/sphere';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { Geometry } from '../../mol-geo/geometry/geometry';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DebugHelperParams = {
|
||||
export const BoundingSphereHelperParams = {
|
||||
sceneBoundingSpheres: PD.Boolean(false, { description: 'Show full scene bounding spheres.' }),
|
||||
visibleSceneBoundingSpheres: PD.Boolean(false, { description: 'Show visible scene bounding spheres.' }),
|
||||
objectBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible render objects.' }),
|
||||
instanceBoundingSpheres: PD.Boolean(false, { description: 'Show bounding spheres of visible instances.' }),
|
||||
};
|
||||
export type DebugHelperParams = typeof DebugHelperParams
|
||||
export type DebugHelperProps = PD.Values<DebugHelperParams>
|
||||
export type BoundingSphereHelperParams = typeof BoundingSphereHelperParams;
|
||||
export type BoundingSphereHelperProps = PD.Values<BoundingSphereHelperParams>;
|
||||
|
||||
type BoundingSphereData = { boundingSphere: Sphere3D, renderObject: GraphicsRenderObject, mesh: Mesh }
|
||||
|
||||
export class BoundingSphereHelper {
|
||||
export class BoundingSphereHelper implements DebugHelper<BoundingSphereHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DebugHelperProps;
|
||||
private _props: BoundingSphereHelperProps;
|
||||
private objectsData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private instancesData = new Map<GraphicsRenderObject, BoundingSphereData>();
|
||||
private sceneData: BoundingSphereData | undefined;
|
||||
private visibleSceneData: BoundingSphereData | undefined;
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DebugHelperProps>) {
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<BoundingSphereHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DebugHelperParams), ...props };
|
||||
this._props = { ...PD.getDefaultValues(BoundingSphereHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
@@ -120,9 +121,9 @@ export class BoundingSphereHelper {
|
||||
this._props.objectBoundingSpheres || this._props.instanceBoundingSpheres
|
||||
);
|
||||
}
|
||||
get props() { return this._props as Readonly<DebugHelperProps>; }
|
||||
get props() { return this._props as Readonly<BoundingSphereHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DebugHelperProps>) {
|
||||
setProps(props: Partial<BoundingSphereHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
@@ -162,4 +163,4 @@ const instanceMaterialId = getNextMaterialId();
|
||||
function createBoundingSphereRenderObject(mesh: Mesh, color: Color, materialId: number, transform?: TransformData) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1, transform);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
}
|
||||
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
403
src/extensions/debug-helpers/clip-object-helper.ts
Normal file
@@ -0,0 +1,403 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { addCylinder } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Box } from '../../mol-geo/primitive/box';
|
||||
import { Plane } from '../../mol-geo/primitive/plane';
|
||||
import { Cylinder } from '../../mol-geo/primitive/cylinder';
|
||||
import { Sphere } from '../../mol-geo/primitive/sphere';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ClipObjectHelperParams = {
|
||||
clipObjects: PD.Boolean(false, { description: 'Show clip-objects of visible render objects.' }),
|
||||
};
|
||||
export type ClipObjectHelperParams = typeof ClipObjectHelperParams;
|
||||
export type ClipObjectHelperProps = PD.Values<ClipObjectHelperParams>;
|
||||
|
||||
//
|
||||
|
||||
/** Serializes clip object params to a string key for deduplication */
|
||||
function clipObjectKey(type: number, invert: boolean, position: ArrayLike<number>, posOffset: number, rotation: ArrayLike<number>, rotOffset: number, scale: ArrayLike<number>, scaleOffset: number, transform: ArrayLike<number>, transformOffset: number): string {
|
||||
// Round floats to 5 decimal places to avoid floating point noise
|
||||
const r = (v: number) => Math.round(v * 100000) / 100000;
|
||||
const parts = [
|
||||
type, invert ? 1 : 0,
|
||||
r(position[posOffset]), r(position[posOffset + 1]), r(position[posOffset + 2]),
|
||||
r(rotation[rotOffset]), r(rotation[rotOffset + 1]), r(rotation[rotOffset + 2]), r(rotation[rotOffset + 3]),
|
||||
r(scale[scaleOffset]), r(scale[scaleOffset + 1]), r(scale[scaleOffset + 2]),
|
||||
];
|
||||
for (let j = 0; j < 16; ++j) {
|
||||
parts.push(r(transform[transformOffset + j]));
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
type ClipObjectData = {
|
||||
key: string,
|
||||
renderObject: GraphicsRenderObject,
|
||||
indicatorRenderObject: GraphicsRenderObject,
|
||||
mesh: Mesh,
|
||||
}
|
||||
|
||||
const clipObjectColors: Record<number, Color> = {
|
||||
[Clip.Type.plane]: ColorNames.orange,
|
||||
[Clip.Type.sphere]: ColorNames.green,
|
||||
[Clip.Type.cube]: ColorNames.dodgerblue,
|
||||
[Clip.Type.cylinder]: ColorNames.gold,
|
||||
[Clip.Type.infiniteCone]: ColorNames.crimson,
|
||||
};
|
||||
|
||||
const clipMaterialId = getNextMaterialId();
|
||||
const indicatorMaterialId = getNextMaterialId();
|
||||
|
||||
// Pre-rotation matrices for aligning primitives to GLSL SDF local frames
|
||||
// Plane: Rx(-90°) maps primitive Z-normal to GLSL Y-normal
|
||||
const preRotPlaneQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), -Math.PI / 2);
|
||||
const preRotPlaneMat = Mat4.fromQuat(Mat4(), preRotPlaneQuat);
|
||||
// Cone: Rx(+90°) maps primitive Y-axis to GLSL Z-axis
|
||||
const preRotConeQuat = Quat.setAxisAngle(Quat(), Vec3.create(1, 0, 0), Math.PI / 2);
|
||||
const preRotConeMat = Mat4.fromQuat(Mat4(), preRotConeQuat);
|
||||
|
||||
// Temp variables for constructing transforms
|
||||
const _position = Vec3();
|
||||
const _rotation = Quat();
|
||||
const _scale = Vec3();
|
||||
const _clipTransform = Mat4();
|
||||
const _invClipTransform = Mat4();
|
||||
const _rotMat = Mat4();
|
||||
const _translateMat = Mat4();
|
||||
const _baseMat = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
const _axisEnd = Vec3();
|
||||
const _yAxis = Vec3.create(0, 1, 0);
|
||||
const _zAxis = Vec3.create(0, 0, 1);
|
||||
const _indicatorPos = Vec3();
|
||||
|
||||
export class ClipObjectHelper implements DebugHelper<ClipObjectHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ClipObjectHelperProps;
|
||||
private objectsData = new Map<string, ClipObjectData>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ClipObjectHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ClipObjectHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const currentKeys = new Set<string>();
|
||||
const sceneRadius = this.parent.boundingSphereVisible.radius || 50;
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
|
||||
const count = ro.values.dClipObjectCount.ref.value;
|
||||
if (count === 0) return;
|
||||
|
||||
const types = ro.values.uClipObjectType.ref.value;
|
||||
const inverts = ro.values.uClipObjectInvert.ref.value;
|
||||
const positions = ro.values.uClipObjectPosition.ref.value;
|
||||
const rotations = ro.values.uClipObjectRotation.ref.value;
|
||||
const scales = ro.values.uClipObjectScale.ref.value;
|
||||
const transforms = ro.values.uClipObjectTransform.ref.value;
|
||||
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const type = types[i];
|
||||
if (type === Clip.Type.none) continue;
|
||||
|
||||
const key = clipObjectKey(
|
||||
type, inverts[i],
|
||||
positions, i * 3,
|
||||
rotations, i * 4,
|
||||
scales, i * 3,
|
||||
transforms, i * 16
|
||||
);
|
||||
|
||||
currentKeys.add(key);
|
||||
|
||||
if (this.objectsData.has(key)) continue;
|
||||
|
||||
// Extract per-object params
|
||||
Vec3.fromArray(_position, positions, i * 3);
|
||||
Quat.fromArray(_rotation, rotations, i * 4);
|
||||
Quat.normalize(_rotation, _rotation); // ensure unit quaternion for proper rotation
|
||||
Vec3.fromArray(_scale, scales, i * 3);
|
||||
Mat4.fromArray(_clipTransform, transforms, i * 16);
|
||||
|
||||
// Build base transform (translate * rotate) without scale,
|
||||
// so each shape can insert pre-rotations before scale.
|
||||
Mat4.fromQuat(_rotMat, _rotation);
|
||||
Mat4.fromTranslation(_translateMat, _position);
|
||||
Mat4.mul(_baseMat, _translateMat, _rotMat);
|
||||
|
||||
// apply inverse of clip transform
|
||||
if (!Mat4.isIdentity(_clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, _clipTransform);
|
||||
Mat4.mul(_baseMat, _invClipTransform, _baseMat);
|
||||
}
|
||||
|
||||
const mesh = createClipObjectMesh(type, _baseMat, _scale, sceneRadius);
|
||||
const color = clipObjectColors[type] || ColorNames.white;
|
||||
const renderObject = createClipObjectRenderObject(mesh, color, clipMaterialId, type);
|
||||
|
||||
// Create position/rotation indicator mesh
|
||||
const invert = inverts[i];
|
||||
const indicatorMesh = createIndicatorMesh(_position, _rotation, _clipTransform, _scale, type, invert);
|
||||
const indicatorRenderObject = createIndicatorRenderObject(indicatorMesh, indicatorMaterialId);
|
||||
|
||||
this.scene.add(renderObject);
|
||||
this.scene.add(indicatorRenderObject);
|
||||
this.objectsData.set(key, { key, renderObject, indicatorRenderObject, mesh });
|
||||
}
|
||||
});
|
||||
|
||||
// Remove clip objects no longer present
|
||||
this.objectsData.forEach((data, key) => {
|
||||
if (!currentKeys.has(key)) {
|
||||
this.scene.remove(data.renderObject);
|
||||
this.scene.remove(data.indicatorRenderObject);
|
||||
this.objectsData.delete(key);
|
||||
}
|
||||
});
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.clipObjects;
|
||||
this.objectsData.forEach(data => {
|
||||
data.renderObject.state.visible = visible;
|
||||
data.indicatorRenderObject.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.objectsData.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.clipObjects;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ClipObjectHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ClipObjectHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createClipObjectMesh(type: number, baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
switch (type) {
|
||||
case Clip.Type.plane: return createPlaneMesh(baseMat, sceneRadius);
|
||||
case Clip.Type.sphere: return createSphereMesh(baseMat, scale);
|
||||
case Clip.Type.cube: return createCubeMesh(baseMat, scale);
|
||||
case Clip.Type.cylinder: return createCylinderMesh(baseMat, scale);
|
||||
case Clip.Type.infiniteCone: return createConeMesh(baseMat, scale, sceneRadius);
|
||||
default: return createSphereMesh(baseMat, scale); // fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plane: GLSL normal is quaternionTransform(rotation, vec3(0,1,0)) — Y-up default.
|
||||
* Plane() primitive lies in XY with normal (0,0,1) along Z.
|
||||
* Pre-rotate Rx(-90°) to align primitive Z-normal to GLSL Y-normal.
|
||||
* Sized to cover the scene bounding sphere. Clip scale is ignored (plane is infinite in GLSL).
|
||||
*/
|
||||
function createPlaneMesh(baseMat: Mat4, sceneRadius: number): Mesh {
|
||||
const size = Math.max(sceneRadius * 2, 10);
|
||||
// baseMat * preRotPlane * uniformScale(size)
|
||||
Mat4.mul(_tmpMat, baseMat, preRotPlaneMat);
|
||||
Mat4.scale(_tmpMat, _tmpMat, Vec3.create(size, size, 1));
|
||||
|
||||
const plane = Plane();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, plane);
|
||||
// Add flipped backface for double-sided visibility
|
||||
MeshBuilder.addPrimitiveFlipped(builderState, _tmpMat, plane);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sphere: SDF uses scale * 0.5 as the radii (ellipsoid).
|
||||
* Sphere primitive has radius 1.
|
||||
* Transform: baseMat * scale * 0.5
|
||||
*/
|
||||
function createSphereMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const detail = 2;
|
||||
const sphere = getSphereForHelper(detail);
|
||||
// baseMat * scale(scale * 0.5)
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1] * 0.5, scale[2] * 0.5));
|
||||
|
||||
const vertexCount = 10 * Math.pow(2, 2 * detail) + 2;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, sphere);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
let _helperSphere: ReturnType<typeof Sphere> | undefined;
|
||||
function getSphereForHelper(detail: number) {
|
||||
if (!_helperSphere) _helperSphere = Sphere(detail);
|
||||
return _helperSphere;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cube: SDF uses scale * 0.5 as half-extents.
|
||||
* Box() primitive is ±0.5 (unit cube), so scaling by `scale` gives half-extents of scale*0.5.
|
||||
*/
|
||||
function createCubeMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
// baseMat * scale(scale)
|
||||
Mat4.scale(_tmpMat, baseMat, scale);
|
||||
|
||||
const box = Box();
|
||||
const builderState = MeshBuilder.createState(256, 128);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, box);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cylinder: SDF axis along Y, radius = scale.x * 0.5, half-height = scale.y * 0.5.
|
||||
* Cylinder primitive: axis along Y, radius=1 in XZ, half-height=0.5 in Y.
|
||||
* Need: X/Z *= scale.x * 0.5 (radius 1 → scale.x*0.5), Y *= scale.y (half-height 0.5 → scale.y*0.5).
|
||||
*/
|
||||
function createCylinderMesh(baseMat: Mat4, scale: Vec3): Mesh {
|
||||
const cyl = Cylinder({ radiusTop: 1, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: true, bottomCap: true });
|
||||
// baseMat * scale(scale.x * 0.5, scale.y, scale.x * 0.5) — use scale.x for both radial axes
|
||||
Mat4.scale(_tmpMat, baseMat, Vec3.create(scale[0] * 0.5, scale[1], scale[0] * 0.5));
|
||||
|
||||
const vertexCount = cyl.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cyl);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
/**
|
||||
* InfiniteCone: GLSL SDF axis along Z, radial in XY.
|
||||
* surface: size.x * length(t.xy) + size.y * t.z = 0 (size = scale * 0.5)
|
||||
* half-angle: tan(θ) = scale.y / scale.x
|
||||
* apex at clip position (origin), opens in -Z direction.
|
||||
*
|
||||
* Cylinder primitive (radiusTop=0, radiusBottom=1, height=1):
|
||||
* axis along Y, tip at Y=+0.5, base at Y=-0.5, base radius=1.
|
||||
*
|
||||
* Transform chain (right-to-left):
|
||||
* 1. Scale(baseRadius, coneLength, baseRadius): stretch primitive to correct proportions
|
||||
* 2. Translate(0, -0.5*coneLength, 0): move tip from Y=+0.5·cL to Y=0 (apex at origin)
|
||||
* (after scale, tip is at Y=+0.5·cL; shifting by -0.5·cL puts it at Y=0)
|
||||
* 3. preRotCone Rx(+90°): map prim-Y→Z, so cone axis becomes Z, opening in -Z
|
||||
* 4. baseMat: position + rotation of clip object
|
||||
*/
|
||||
function createConeMesh(baseMat: Mat4, scale: Vec3, sceneRadius: number): Mesh {
|
||||
const cone = Cylinder({ radiusTop: 0, radiusBottom: 1, height: 1, radialSegments: 16, heightSegments: 1, topCap: false, bottomCap: true });
|
||||
|
||||
// Visible length of the (infinite) cone, and base radius matching the GLSL half-angle
|
||||
const coneLength = Math.max(sceneRadius * 2, 10);
|
||||
const tanHalfAngle = (scale[1] || 1) / (scale[0] || 1); // tan(θ) = scaleY / scaleX
|
||||
const baseRadius = coneLength * tanHalfAngle;
|
||||
|
||||
// baseMat * preRotCone * Translate(0, -coneLength/2, 0) * Scale(baseRadius, coneLength, baseRadius)
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), Vec3.create(baseRadius, coneLength, baseRadius));
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), Vec3.create(0, -coneLength * 0.5, 0));
|
||||
Mat4.mul(_tmpMat, translateMat, scaleMat);
|
||||
Mat4.mul(_tmpMat, preRotConeMat, _tmpMat);
|
||||
Mat4.mul(_tmpMat, baseMat, _tmpMat);
|
||||
|
||||
const vertexCount = cone.vertices.length / 3;
|
||||
const builderState = MeshBuilder.createState(vertexCount * 3, vertexCount);
|
||||
MeshBuilder.addPrimitive(builderState, _tmpMat, cone);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createClipObjectRenderObject(mesh: Mesh, color: Color, materialId: number, type: number) {
|
||||
const alpha = type === Clip.Type.plane ? 0.25 : 0.15;
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha, doubleSided: false, cellSize: 0, batchSize: 0 }, color, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mesh with a sphere at the clip object position and a cylinder
|
||||
* along the characteristic axis to indicate orientation.
|
||||
*
|
||||
* - Plane/sphere/cube/cylinder: axis = rotated Y (matches GLSL normal/axis direction)
|
||||
* - InfiniteCone: axis = rotated Z (cone axis is Z in local frame)
|
||||
* - Plane with invert: direction is flipped
|
||||
*/
|
||||
function createIndicatorMesh(position: Vec3, rotation: Quat, clipTransform: Mat4, scale: Vec3, type: number, invert: boolean): Mesh {
|
||||
const objectSize = Math.max(scale[0], scale[1], scale[2]);
|
||||
const sphereRadius = Math.max(objectSize * 0.004, 0.01);
|
||||
const cylinderRadius = sphereRadius * 0.4;
|
||||
const axisLength = Math.max(objectSize * 0.1, 2);
|
||||
|
||||
// Transform position by inverse of clipTransform if non-identity
|
||||
Vec3.copy(_indicatorPos, position);
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
Mat4.invert(_invClipTransform, clipTransform);
|
||||
Vec3.transformMat4(_indicatorPos, _indicatorPos, _invClipTransform);
|
||||
}
|
||||
|
||||
// Choose the local-frame axis based on clip type
|
||||
const localAxis = type === Clip.Type.infiniteCone ? _zAxis : _yAxis;
|
||||
Vec3.transformQuat(_axisEnd, localAxis, rotation);
|
||||
|
||||
// Cone opens in -Z locally, so negate to point along the cone opening
|
||||
if (type === Clip.Type.infiniteCone) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// For planes, the normal points toward the clipped (removed) side.
|
||||
// Flip so the indicator points toward the non-clipped (kept) geometry.
|
||||
// When inverted, the kept side is the normal side, so don't flip.
|
||||
if (type === Clip.Type.plane && !invert) {
|
||||
Vec3.negate(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// If clipTransform is non-identity, also transform the axis direction
|
||||
if (!Mat4.isIdentity(clipTransform)) {
|
||||
// Transform direction (not position) by inverse clipTransform
|
||||
const endWorld = Vec3();
|
||||
Vec3.add(endWorld, position, Vec3.scale(Vec3(), _axisEnd, axisLength));
|
||||
Vec3.transformMat4(endWorld, endWorld, _invClipTransform);
|
||||
Vec3.sub(_axisEnd, endWorld, _indicatorPos);
|
||||
Vec3.normalize(_axisEnd, _axisEnd);
|
||||
}
|
||||
|
||||
// Axis cylinder endpoint
|
||||
const axisEndPoint = Vec3();
|
||||
Vec3.scaleAndAdd(axisEndPoint, _indicatorPos, _axisEnd, axisLength);
|
||||
|
||||
const builderState = MeshBuilder.createState(512, 256);
|
||||
// Position sphere
|
||||
addSphere(builderState, _indicatorPos, sphereRadius, 1);
|
||||
// Rotation axis cylinder
|
||||
addCylinder(builderState, _indicatorPos, axisEndPoint, 1, { radiusTop: cylinderRadius, radiusBottom: cylinderRadius, radialSegments: 8 });
|
||||
// Small sphere at tip of axis
|
||||
addSphere(builderState, axisEndPoint, cylinderRadius * 1.5, 1);
|
||||
return MeshBuilder.getMesh(builderState);
|
||||
}
|
||||
|
||||
function createIndicatorRenderObject(mesh: Mesh, materialId: number) {
|
||||
const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.7, doubleSided: false, cellSize: 0, batchSize: 0 }, ColorNames.white, 1);
|
||||
return createRenderObject('mesh', values, { disposed: false, visible: true, alphaFactor: 1, pickable: false, colorOnly: false, opaque: false, writeDepth: false }, materialId);
|
||||
}
|
||||
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
145
src/extensions/debug-helpers/direct-volume-helper.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { DirectVolumeValues } from '../../mol-gl/renderable/direct-volume';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const DirectVolumeHelperParams = {
|
||||
directVolumeEdges: PD.Boolean(false, { description: 'Show edges of visible direct-volume render objects.' }),
|
||||
};
|
||||
export type DirectVolumeHelperParams = typeof DirectVolumeHelperParams;
|
||||
export type DirectVolumeHelperProps = PD.Values<DirectVolumeHelperParams>;
|
||||
|
||||
const directVolumeMaterialId = getNextMaterialId();
|
||||
|
||||
type TrackedEntry = { ro: GraphicsRenderObject, version: number };
|
||||
|
||||
export class DirectVolumeHelper implements DebugHelper<DirectVolumeHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: DirectVolumeHelperProps;
|
||||
private renderObjects = new Map<number, TrackedEntry>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<DirectVolumeHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(DirectVolumeHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'direct-volume') return;
|
||||
|
||||
const values = ro.values as DirectVolumeValues;
|
||||
const version = values.uUnitToCartn.ref.version + values.uGridDim.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entry if version changed
|
||||
if (existing) {
|
||||
this.scene.remove(existing.ro);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const lines = createVolumeEdgeLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createLinesRenderObject(lines, directVolumeMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, { ro: linesRO, version });
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
this.scene.remove(entry.ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.directVolumeEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
entry.ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.directVolumeEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<DirectVolumeHelperProps>; }
|
||||
|
||||
setProps(props: Partial<DirectVolumeHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* The volume proxy box in the shader uses aPosition in [-0.5, 0.5]^3,
|
||||
* shifted to [0,1]^3 (unitCoord = aPosition + 0.5), then transformed by:
|
||||
* uUnitToCartn → Cartesian space
|
||||
* aTransform → instance space
|
||||
*
|
||||
* We replicate this pipeline to get the correct world-space edges.
|
||||
* Grid ticks are placed at 1/gridDim intervals along each edge.
|
||||
*/
|
||||
function createVolumeEdgeLines(values: DirectVolumeValues): Lines | undefined {
|
||||
const unitToCartn = values.uUnitToCartn.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
if (bs.radius < 1e-6) return undefined;
|
||||
|
||||
const builder = LinesBuilder.create(128 * instanceCount);
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
// Combined transform: aTransform * uUnitToCartn
|
||||
const combined = Mat4.mul(Mat4(), instTransform, unitToCartn);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.8 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.orange, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
266
src/extensions/debug-helpers/image-helper.ts
Normal file
266
src/extensions/debug-helpers/image-helper.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Quat } from '../../mol-math/linear-algebra/3d/quat';
|
||||
import { ImageValues } from '../../mol-gl/renderable/image';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { addSphere as addLinesSphere } from '../../mol-geo/geometry/lines/builder/sphere';
|
||||
import { addBox } from '../../mol-geo/geometry/lines/builder/box';
|
||||
import { addPlane } from '../../mol-geo/geometry/lines/builder/plane';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const ImageHelperParams = {
|
||||
imageEdges: PD.Boolean(false, { description: 'Show edges of visible image render objects.' }),
|
||||
};
|
||||
export type ImageHelperParams = typeof ImageHelperParams;
|
||||
export type ImageHelperProps = PD.Values<ImageHelperParams>;
|
||||
|
||||
const imageEdgeMaterialId = getNextMaterialId();
|
||||
const imageTrimMaterialId = getNextMaterialId();
|
||||
|
||||
// Temp vectors
|
||||
const _trimPos = Vec3();
|
||||
const _trimScale = Vec3();
|
||||
const _trimRot = Quat();
|
||||
const _trimTransform = Mat4();
|
||||
const _tmpMat = Mat4();
|
||||
|
||||
export class ImageHelper implements DebugHelper<ImageHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: ImageHelperProps;
|
||||
private renderObjects = new Map<number, { roList: GraphicsRenderObject[], version: number }>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<ImageHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(ImageHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'image') return;
|
||||
|
||||
const values = ro.values as ImageValues;
|
||||
const version = values.aPosition.ref.version
|
||||
+ values.uTrimType.ref.version + values.uTrimCenter.ref.version
|
||||
+ values.uTrimRotation.ref.version + values.uTrimScale.ref.version
|
||||
+ values.uTrimTransform.ref.version + values.aTransform.ref.version;
|
||||
|
||||
const existing = this.renderObjects.get(ro.id);
|
||||
if (existing && existing.version === version) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove old entries if version changed
|
||||
if (existing) {
|
||||
for (const oldRO of existing.roList) this.scene.remove(oldRO);
|
||||
this.renderObjects.delete(ro.id);
|
||||
}
|
||||
|
||||
const roList: GraphicsRenderObject[] = [];
|
||||
|
||||
const edgeLines = createImageEdgeLines(values);
|
||||
if (edgeLines) {
|
||||
const edgeRO = createLinesRenderObject(edgeLines, imageEdgeMaterialId, ColorNames.cyan, 0.8);
|
||||
this.scene.add(edgeRO);
|
||||
roList.push(edgeRO);
|
||||
}
|
||||
|
||||
const trimLines = createTrimEdgeLines(values);
|
||||
if (trimLines) {
|
||||
const trimRO = createLinesRenderObject(trimLines, imageTrimMaterialId, ColorNames.yellow, 0.7);
|
||||
this.scene.add(trimRO);
|
||||
roList.push(trimRO);
|
||||
}
|
||||
|
||||
if (roList.length > 0) {
|
||||
this.renderObjects.set(ro.id, { roList, version });
|
||||
}
|
||||
previousIds.delete(ro.id);
|
||||
});
|
||||
|
||||
for (const id of previousIds) {
|
||||
const entry = this.renderObjects.get(id);
|
||||
if (entry) {
|
||||
for (const ro of entry.roList) this.scene.remove(ro);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.imageEdges;
|
||||
this.renderObjects.forEach(entry => {
|
||||
for (const ro of entry.roList) ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.imageEdges;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<ImageHelperProps>; }
|
||||
|
||||
setProps(props: Partial<ImageHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
/**
|
||||
* Image quad vertex layout (from image.ts):
|
||||
* Vertex 0: UV (0,1) — top-left
|
||||
* Vertex 1: UV (0,0) — bottom-left
|
||||
* Vertex 2: UV (1,1) — top-right
|
||||
* Vertex 3: UV (1,0) — bottom-right
|
||||
*
|
||||
* addPlane expects corners in winding order (0→1→2→3→0),
|
||||
* so we reorder to: top-left, bottom-left, bottom-right, top-right.
|
||||
*/
|
||||
const _planeCorners = new Float32Array(12);
|
||||
|
||||
function createImageEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
if (positions.length < 12) return undefined; // need 4 vertices × 3 components
|
||||
|
||||
// Reorder from [TL, BL, TR, BR] to winding order [TL, BL, BR, TR]
|
||||
// V0 (TL) → slot 0
|
||||
_planeCorners[0] = positions[0]; _planeCorners[1] = positions[1]; _planeCorners[2] = positions[2];
|
||||
// V1 (BL) → slot 1
|
||||
_planeCorners[3] = positions[3]; _planeCorners[4] = positions[4]; _planeCorners[5] = positions[5];
|
||||
// V3 (BR) → slot 2
|
||||
_planeCorners[6] = positions[9]; _planeCorners[7] = positions[10]; _planeCorners[8] = positions[11];
|
||||
// V2 (TR) → slot 3
|
||||
_planeCorners[9] = positions[6]; _planeCorners[10] = positions[7]; _planeCorners[11] = positions[8];
|
||||
|
||||
const builder = LinesBuilder.create(4 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, inst * 16);
|
||||
addPlane(builder, _planeCorners, transform, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createTrimEdgeLines(values: ImageValues): Lines | undefined {
|
||||
const trimType = values.uTrimType.ref.value as number;
|
||||
if (trimType === 0) return undefined; // no trim
|
||||
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
Vec3.copy(_trimPos, values.uTrimCenter.ref.value);
|
||||
Quat.copy(_trimRot, values.uTrimRotation.ref.value);
|
||||
Vec3.copy(_trimScale, values.uTrimScale.ref.value);
|
||||
Mat4.copy(_trimTransform, values.uTrimTransform.ref.value);
|
||||
|
||||
if (trimType === Clip.Type.cube) {
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
} else if (trimType === Clip.Type.sphere) {
|
||||
return createSphereTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
// For other trim types (plane, cylinder, cone), draw a cube outline as a fallback
|
||||
// using the trim center/scale/rotation
|
||||
return createCubeTrimLines(transforms, instanceCount);
|
||||
}
|
||||
|
||||
function createCubeTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
// Build cube transform: translate * rotate * scale
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
const scaleMat = Mat4.fromScaling(Mat4(), _trimScale);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
Mat4.mul(_tmpMat, _tmpMat, scaleMat);
|
||||
|
||||
// Apply inverse of trim transform
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
// addBox uses [0,1]^3, trim cube uses [-0.5,0.5]^3 — prepend offset
|
||||
const offset = Mat4.fromTranslation(Mat4(), Vec3.create(-0.5, -0.5, -0.5));
|
||||
Mat4.mul(_tmpMat, _tmpMat, offset);
|
||||
|
||||
const builder = LinesBuilder.create(12 * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
addBox(builder, combined, 0);
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createSphereTrimLines(transforms: Float32Array, instanceCount: number): Lines | undefined {
|
||||
const radius = Math.max(_trimScale[0] * 0.5, _trimScale[1] * 0.5, _trimScale[2] * 0.5);
|
||||
|
||||
const rotMat = Mat4.fromQuat(Mat4(), _trimRot);
|
||||
const translateMat = Mat4.fromTranslation(Mat4(), _trimPos);
|
||||
Mat4.mul(_tmpMat, translateMat, rotMat);
|
||||
|
||||
if (!Mat4.isIdentity(_trimTransform)) {
|
||||
const invTrimTransform = Mat4.invert(Mat4(), _trimTransform);
|
||||
Mat4.mul(_tmpMat, invTrimTransform, _tmpMat);
|
||||
}
|
||||
|
||||
const segments = 32;
|
||||
const circlesPerDimension = 3;
|
||||
const builder = LinesBuilder.create(segments * 3 * circlesPerDimension * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const instTransform = Mat4();
|
||||
Mat4.fromArray(instTransform, transforms, inst * 16);
|
||||
const combined = Mat4.mul(Mat4(), instTransform, _tmpMat);
|
||||
|
||||
addLinesSphere(builder, radius, combined, 0, { segments, circlesPerDimension });
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createLinesRenderObject(lines: Lines, materialId: number, color: Color, alpha: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, color, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
71
src/extensions/debug-helpers/index.ts
Normal file
71
src/extensions/debug-helpers/index.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { BoundingSphereHelper, BoundingSphereHelperParams } from './bounding-sphere-helper';
|
||||
import { ClipObjectHelper, ClipObjectHelperParams } from './clip-object-helper';
|
||||
import { DirectVolumeHelper, DirectVolumeHelperParams } from './direct-volume-helper';
|
||||
import { ImageHelper, ImageHelperParams } from './image-helper';
|
||||
import { MeshHelper, MeshHelperParams } from './mesh-helper';
|
||||
|
||||
const DebugHelpersParams = {
|
||||
...BoundingSphereHelperParams,
|
||||
...ClipObjectHelperParams,
|
||||
...MeshHelperParams,
|
||||
...ImageHelperParams,
|
||||
...DirectVolumeHelperParams,
|
||||
};
|
||||
type DebugHelpersParams = typeof DebugHelpersParams;
|
||||
type DebugHelpersProps = PD.Values<DebugHelpersParams>;
|
||||
|
||||
export const DebugHelpers = PluginBehavior.create<DebugHelpersProps>({
|
||||
name: 'extension-debug-helpers',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'Debug Helpers'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<DebugHelpersProps> {
|
||||
async register(): Promise<void> {
|
||||
await this.ctx.canvas3dInitialized;
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
const { ctx, parent } = dr;
|
||||
|
||||
dr.register('bounding-sphere', new BoundingSphereHelper(ctx, parent, this.params));
|
||||
dr.register('clip-object', new ClipObjectHelper(ctx, parent, this.params));
|
||||
dr.register('mesh', new MeshHelper(ctx, parent, this.params));
|
||||
dr.register('image', new ImageHelper(ctx, parent, this.params));
|
||||
dr.register('direct-volume', new DirectVolumeHelper(ctx, parent, this.params));
|
||||
}
|
||||
|
||||
update(params: DebugHelpersProps) {
|
||||
const changed = super.update(params);
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (changed && canvas3d) {
|
||||
canvas3d.debugRegistry.setProps(params);
|
||||
canvas3d.requestDraw();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
const canvas3d = this.ctx.canvas3d;
|
||||
if (!canvas3d) return;
|
||||
|
||||
const dr = canvas3d.debugRegistry;
|
||||
dr.unregister('bounding-sphere');
|
||||
dr.unregister('clip-object');
|
||||
dr.unregister('mesh');
|
||||
dr.unregister('image');
|
||||
dr.unregister('direct-volume');
|
||||
}
|
||||
},
|
||||
params: () => DebugHelpersParams,
|
||||
canAutoUpdate: () => true,
|
||||
});
|
||||
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
164
src/extensions/debug-helpers/mesh-helper.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { createRenderObject, GraphicsRenderObject, getNextMaterialId } from '../../mol-gl/render-object';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { MeshValues } from '../../mol-gl/renderable/mesh';
|
||||
import { DebugHelper } from '../../mol-canvas3d/helper/debug-registry';
|
||||
|
||||
export const MeshHelperParams = {
|
||||
meshNormals: PD.Boolean(false, { description: 'Show normals of visible mesh render objects.' }),
|
||||
};
|
||||
export type MeshHelperParams = typeof MeshHelperParams;
|
||||
export type MeshHelperProps = PD.Values<MeshHelperParams>;
|
||||
|
||||
const meshHelperMaterialId = getNextMaterialId();
|
||||
|
||||
const _v = Vec3();
|
||||
const _n = Vec3();
|
||||
const _start = Vec3();
|
||||
const _end = Vec3();
|
||||
|
||||
export class MeshHelper implements DebugHelper<MeshHelperProps> {
|
||||
readonly scene: Scene;
|
||||
|
||||
private readonly parent: Scene;
|
||||
private _props: MeshHelperProps;
|
||||
private renderObjects = new Map<number, GraphicsRenderObject>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene, props: Partial<MeshHelperProps>) {
|
||||
this.scene = Scene.create(ctx, 'blended');
|
||||
this.parent = parent;
|
||||
this._props = { ...PD.getDefaultValues(MeshHelperParams), ...props };
|
||||
}
|
||||
|
||||
update() {
|
||||
const previousIds = new Set(this.renderObjects.keys());
|
||||
const currentIds = new Set<number>();
|
||||
|
||||
this.parent.forEach((r, ro) => {
|
||||
if (!ro.state.visible) return;
|
||||
if (ro.type !== 'mesh') return;
|
||||
|
||||
currentIds.add(ro.id);
|
||||
|
||||
// Skip if we already have normals for this render object
|
||||
if (this.renderObjects.has(ro.id)) {
|
||||
previousIds.delete(ro.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const values = ro.values as MeshValues;
|
||||
const lines = createNormalLines(values);
|
||||
if (!lines) return;
|
||||
|
||||
const linesRO = createNormalLinesRenderObject(lines, meshHelperMaterialId);
|
||||
this.scene.add(linesRO);
|
||||
this.renderObjects.set(ro.id, linesRO);
|
||||
});
|
||||
|
||||
// Remove normals for render objects no longer present
|
||||
for (const id of previousIds) {
|
||||
const linesRO = this.renderObjects.get(id);
|
||||
if (linesRO) {
|
||||
this.scene.remove(linesRO);
|
||||
this.renderObjects.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.update(void 0, false);
|
||||
this.scene.commit();
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
const visible = this._props.meshNormals;
|
||||
this.renderObjects.forEach(ro => {
|
||||
ro.state.visible = visible;
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.renderObjects.clear();
|
||||
this.scene.clear();
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
return this._props.meshNormals;
|
||||
}
|
||||
|
||||
get props() { return this._props as Readonly<MeshHelperProps>; }
|
||||
|
||||
setProps(props: Partial<MeshHelperProps>) {
|
||||
Object.assign(this._props, props);
|
||||
if (this.isEnabled) this.update();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
function createNormalLines(values: MeshValues): Lines | undefined {
|
||||
const positions = values.aPosition.ref.value;
|
||||
const normals = values.aNormal.ref.value;
|
||||
const indices = values.elements.ref.value;
|
||||
const transforms = values.aTransform.ref.value;
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
const vertexCount = positions.length / 3;
|
||||
if (vertexCount === 0) return undefined;
|
||||
|
||||
// Determine normal line length: proportional to bounding sphere radius
|
||||
const bs = values.boundingSphere.ref.value;
|
||||
const normalLength = Math.max(bs.radius * 0.01, 0.1);
|
||||
|
||||
// Count unique vertices referenced by indices
|
||||
const indexCount = values.drawCount.ref.value;
|
||||
|
||||
const builder = LinesBuilder.create(indexCount * instanceCount);
|
||||
|
||||
for (let inst = 0; inst < instanceCount; ++inst) {
|
||||
const tOffset = inst * 16;
|
||||
const transform = Mat4();
|
||||
Mat4.fromArray(transform, transforms, tOffset);
|
||||
|
||||
// Use a set to avoid drawing duplicate normals for shared vertices
|
||||
const visited = new Set<number>();
|
||||
|
||||
for (let i = 0; i < indexCount; ++i) {
|
||||
const vi = indices[i];
|
||||
if (visited.has(vi)) continue;
|
||||
visited.add(vi);
|
||||
|
||||
const vo = vi * 3;
|
||||
Vec3.set(_v, positions[vo], positions[vo + 1], positions[vo + 2]);
|
||||
Vec3.set(_n, normals[vo], normals[vo + 1], normals[vo + 2]);
|
||||
|
||||
// Transform vertex position and normal direction by instance transform
|
||||
Vec3.transformMat4(_start, _v, transform);
|
||||
Vec3.transformDirection(_end, _n, transform);
|
||||
Vec3.normalize(_end, _end);
|
||||
Vec3.scaleAndAdd(_end, _start, _end, normalLength);
|
||||
|
||||
builder.addVec(_start, _end, 0);
|
||||
}
|
||||
}
|
||||
|
||||
return builder.getLines();
|
||||
}
|
||||
|
||||
function createNormalLinesRenderObject(lines: Lines, materialId: number): GraphicsRenderObject {
|
||||
const props = { ...PD.getDefaultValues(Lines.Params), sizeFactor: 1, alpha: 0.7 };
|
||||
const values = Lines.Utils.createValuesSimple(lines, props, ColorNames.magenta, 1);
|
||||
const state = Lines.Utils.createRenderableState(props);
|
||||
state.pickable = false;
|
||||
return createRenderObject('lines', values, state, materialId);
|
||||
}
|
||||
@@ -59,7 +59,7 @@ export async function getG3dDataBlock(ctx: PluginContext, header: G3dHeader, url
|
||||
|
||||
async function getRawData(ctx: PluginContext, urlOrData: string | Uint8Array, range: { offset: number, size: number }) {
|
||||
if (typeof urlOrData === 'string') {
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: [['Range', `bytes=${range.offset}-${range.offset + range.size - 1}`]], type: 'binary' }));
|
||||
return await ctx.runTask(ctx.fetch({ url: urlOrData, headers: { 'Range': `bytes=${range.offset}-${range.offset + range.size - 1}` }, type: 'binary' }));
|
||||
} else {
|
||||
return urlOrData.slice(range.offset, range.offset + range.size);
|
||||
}
|
||||
|
||||
28
src/extensions/kinemage/README.md
Normal file
28
src/extensions/kinemage/README.md
Normal file
@@ -0,0 +1,28 @@
|
||||
# Kinemage extension
|
||||
|
||||
This extension adds support for the Kinemage molecular graphics format based on the
|
||||
[kinemage format specification](http://kinemage.biochem.duke.edu/static/files/PDFs/format-kinemage.pdf).
|
||||
|
||||
It currently supports the following features:
|
||||
- Drag-and-drop of Kinemage files into the display area
|
||||
- Open File can open Kinemage files from the local filesystem
|
||||
- Display of @ball, @sphere, @vector, @dot, @ribbon, and @triangle lists
|
||||
- Coloring of objects by vertex color, or by a single color for the entire list
|
||||
- Hovering over objects to see their labels (if present)
|
||||
- When there are views defined, controls are added to the right panel; when selected, they shift the view
|
||||
- When the view is changes, the projection is set to orthographic and the background is set to black to match Kinemage's default view
|
||||
- Control panel names are based on the @pdbfile or @caption in the Kinemage file if there is one
|
||||
- Lines are split in half, with each half colored by and labeled by the nearest vertex
|
||||
- Master and submaster selections of visible objects
|
||||
- Group and subgroup hierarchy with buttons to control visibility
|
||||
- @pointmaster lists controlling visibility of points
|
||||
- animate/2animate: First entry turned on to start, changing visibility of Animate button cycles through them
|
||||
|
||||
Currently unsupported features include:
|
||||
- @label and @ring lists
|
||||
- @hsvcolor keyword for coloring by hue, saturation, and value
|
||||
- 'fore' and 'rear' keywords for different front and back colors
|
||||
|
||||
Current limitations include:
|
||||
- Triangles are a single color, not colored by vertex (Mol* does not support per-vertex coloring for these primitives)
|
||||
- Line segments in Mol* do not support end-caps for wide lines, so there are artifacts in highly-curved lines
|
||||
70
src/extensions/kinemage/_spec/kin.spec.ts
Normal file
70
src/extensions/kinemage/_spec/kin.spec.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { parseKin } from '../reader/parser';
|
||||
|
||||
const kinString = `@kinemage 1
|
||||
@caption probe.2.26.021123, run Tue Apr 23 14:49:17 2024
|
||||
command: C:\tmp\cctbx_phenix\build\probe\exe\probe.exe -kin -mc -het -once -wat2wat -onlybadout -stdbonds water all 1ssxFH.pdb
|
||||
@group dominant {dots}
|
||||
@subgroup dominant {once dots}
|
||||
@master {bad overlap}
|
||||
@pointmaster 'O' {Hets contacts}
|
||||
@vectorlist {x} color=red master={bad overlap}
|
||||
{ O HOH 319 A}hotpink P 'O' 31.146,32.100,-1.425 {"}hotpink 'O' 31.015,32.234,-1.324
|
||||
{"}hotpink P 'O' 31.607,32.750,-1.156 {"}hotpink 'O' 31.410,32.784,-1.097
|
||||
{"}hotpink P 'O' 31.263,32.074,-1.185 {"}hotpink 'O' 31.117,32.209,-1.122
|
||||
{ O BHOH 338 A}hotpink P 'O' 32.540,45.631,10.833 {"}hotpink 'O' 32.430,45.771,10.977
|
||||
{"}hotpink P 'O' 32.316,45.500,10.828 {"}hotpink 'O' 32.230,45.689,10.998
|
||||
{"}hotpink P 'O' 32.068,45.424,10.824 {"}hotpink 'O' 32.034,45.604,10.975
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
// @todo Replace with more complex kinemage
|
||||
const kinComplexString = `@kinemage 1
|
||||
@caption probe.2.26.021123, run Tue Apr 23 14:49:17 2024
|
||||
command: C:\tmp\cctbx_phenix\build\probe\exe\probe.exe -kin -mc -het -once -wat2wat -onlybadout -stdbonds water all 1ssxFH.pdb
|
||||
@group dominant {dots}
|
||||
@subgroup dominant {once dots}
|
||||
@master {bad overlap}
|
||||
@pointmaster 'O' {Hets contacts}
|
||||
@vectorlist {x} color=red master={bad overlap}
|
||||
{ O HOH 319 A}hotpink P 'O' 31.146,32.100,-1.425 {"}hotpink 'O' 31.015,32.234,-1.324
|
||||
{"}hotpink P 'O' 31.607,32.750,-1.156 {"}hotpink 'O' 31.410,32.784,-1.097
|
||||
{"}hotpink P 'O' 31.263,32.074,-1.185 {"}hotpink 'O' 31.117,32.209,-1.122
|
||||
{ O BHOH 338 A}hotpink P 'O' 32.540,45.631,10.833 {"}hotpink 'O' 32.430,45.771,10.977
|
||||
{"}hotpink P 'O' 32.316,45.500,10.828 {"}hotpink 'O' 32.230,45.689,10.998
|
||||
{"}hotpink P 'O' 32.068,45.424,10.824 {"}hotpink 'O' 32.034,45.604,10.975
|
||||
{"}hotpink P 'O' 32.729,45.605,11.052 {"}hotpink 'O' 32.572,45.765,11.173
|
||||
`;
|
||||
|
||||
describe('kin reader', () => {
|
||||
it('basic', async () => {
|
||||
const parsed = await parseKin(kinString).run();
|
||||
if (parsed.isError) return;
|
||||
if (parsed.result.length !== 1) return;
|
||||
const kinemage = parsed.result[0];
|
||||
|
||||
const vectors = kinemage.vectorLists;
|
||||
expect(vectors.length).toEqual(1);
|
||||
|
||||
const element = vectors[0];
|
||||
expect(element.name).toEqual('x');
|
||||
expect(element.position1Array.length).toEqual(7);
|
||||
|
||||
// TODO: Add more tests
|
||||
|
||||
expect.assertions(3);
|
||||
});
|
||||
|
||||
it('complex', async () => {
|
||||
const parsed = await parseKin(kinComplexString).run();
|
||||
if (parsed.isError) return;
|
||||
|
||||
// TODO: Add more complex tests
|
||||
|
||||
});
|
||||
});
|
||||
548
src/extensions/kinemage/behavior.ts
Normal file
548
src/extensions/kinemage/behavior.ts
Normal file
@@ -0,0 +1,548 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { Vec3, Mat3 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { KinemageDataProvider, KinemageData } from './prop';
|
||||
import { StateTransformer, StateBuilder } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior';
|
||||
import { PluginDragAndDropHandler } from '../../mol-plugin-state/manager/drag-and-drop';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { DefaultQueryRuntimeTable } from '../../mol-script/runtime/query/compiler';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { shapePointsFromKin, shapeLinesFromKin, shapeMeshFromKin, shapeSpheresFromKin } from './kin';
|
||||
import { Kinemage } from './reader/schema';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { KinemageControls } from './ui';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { Color } from '../../mol-util/color';
|
||||
|
||||
const Tag = KinemageData.Tag;
|
||||
|
||||
const Transform = StateTransformer.builderFactory('sb-kinemage');
|
||||
|
||||
/**
|
||||
* State object to hold parsed Kinemage data
|
||||
*/
|
||||
export class KinemageObject extends PluginStateObject.Create<KinemageData>({ name: 'Kinemage', typeClass: 'Object' }) { }
|
||||
|
||||
/**
|
||||
* Apply a saved snapshot object (from a view state node) to the plugin camera.
|
||||
* Use PluginCommands.Camera.SetSnapshot so transitions and canvas props are handled properly.
|
||||
*/
|
||||
export async function applyViewSnapshot(plugin: PluginContext, snapshot: Partial<Camera.Snapshot>) {
|
||||
if (!snapshot) return;
|
||||
|
||||
// Set background color to black
|
||||
plugin.canvas3d?.setProps({
|
||||
renderer: {
|
||||
...plugin.canvas3d.props.renderer,
|
||||
backgroundColor: Color(0x000000)
|
||||
}
|
||||
});
|
||||
|
||||
// If the snapshot provides a target, adjust the canvas `sceneRadiusFactor` so the scene isn't clipped
|
||||
// when we switch camera.
|
||||
if (snapshot.target) {
|
||||
try {
|
||||
const boundingSphere = getPluginBoundingSphere(plugin);
|
||||
if (boundingSphere && boundingSphere.radius > 0) {
|
||||
const offset = Vec3.distance(snapshot.target as Vec3, boundingSphere.center);
|
||||
const sceneRadiusFactor = (boundingSphere.radius + offset) / boundingSphere.radius;
|
||||
plugin.canvas3d?.setProps({ sceneRadiusFactor });
|
||||
}
|
||||
} catch (e) {
|
||||
// fallback: ignore errors and continue to set the camera snapshot
|
||||
console.warn('Failed to adjust sceneRadiusFactor for view snapshot', e);
|
||||
}
|
||||
}
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform to parse Kinemage data from string/data input
|
||||
*/
|
||||
export const ParseKinemage = Transform({
|
||||
name: 'sb-kinemage-parse',
|
||||
display: { name: 'Parse Kinemage' },
|
||||
from: [PluginStateObject.Data.String],
|
||||
to: KinemageObject,
|
||||
params: {
|
||||
label: PD.Optional(PD.Text('', { description: 'Label for the Kinemage data' }))
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Parse Kinemage', async ctx => {
|
||||
const input = a.data;
|
||||
let data: KinemageData;
|
||||
|
||||
if (typeof input === 'string') {
|
||||
// Parse from string content
|
||||
const file = new File([input], 'input.kin', { type: 'text/plain' });
|
||||
data = await KinemageData.open(file);
|
||||
} else {
|
||||
throw new Error('Unsupported input type for ParseKinemage');
|
||||
}
|
||||
|
||||
// Precompute camera snapshots for all views in all kinemages
|
||||
for (const kinData of data.kinemages) {
|
||||
(kinData as any).viewSnapshots = (kinData as any).viewSnapshots || Object.create(null);
|
||||
for (const [viewKey, viewObj] of Object.entries(kinData.viewDict)) {
|
||||
const center = Vec3.create(0, 0, 0);
|
||||
if (viewObj.center) {
|
||||
Vec3.set(center, viewObj.center[0], viewObj.center[1], viewObj.center[2]);
|
||||
}
|
||||
|
||||
const orientation: Mat3 = Mat3.identity();
|
||||
if (viewObj.matrix) {
|
||||
Mat3.fromArray(orientation, viewObj.matrix, 0);
|
||||
Mat3.transpose(orientation, orientation);
|
||||
}
|
||||
|
||||
const zAxis = Vec3.create(0, 0, 1);
|
||||
Vec3.transformMat3(zAxis, zAxis, orientation);
|
||||
|
||||
const yAxis = Vec3.create(0, 1, 0);
|
||||
Vec3.transformMat3(yAxis, yAxis, orientation);
|
||||
|
||||
let distance = 100;
|
||||
if (viewObj.span) {
|
||||
distance = viewObj.span;
|
||||
}
|
||||
Vec3.scale(zAxis, zAxis, distance);
|
||||
const position = Vec3.create(0, 0, 100);
|
||||
Vec3.add(position, center, zAxis);
|
||||
|
||||
let radius = 100;
|
||||
if (viewObj.zslab) {
|
||||
const scale = viewObj.zslab / 200;
|
||||
radius = 0.5 * distance * scale;
|
||||
}
|
||||
|
||||
const snap: Camera.Snapshot = {
|
||||
mode: 'orthographic',
|
||||
fov: Math.PI / 4,
|
||||
position,
|
||||
up: yAxis,
|
||||
target: center,
|
||||
radius,
|
||||
radiusMax: 1e4,
|
||||
fog: 0,
|
||||
clipFar: true,
|
||||
minNear: 1,
|
||||
minFar: 1
|
||||
};
|
||||
|
||||
(kinData as any).viewSnapshots[viewKey] = snap;
|
||||
}
|
||||
}
|
||||
|
||||
const label = params.label || data.kinemages[0]?.caption || 'Kinemage';
|
||||
return new KinemageObject(data, { label, description: `Kinemage with ${data.kinemages.length} view(s)` });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Transform to select a specific kinemage from parsed data
|
||||
*/
|
||||
export const SelectKinemage = Transform({
|
||||
name: 'sb-kinemage-select',
|
||||
display: { name: 'Select Kinemage' },
|
||||
from: KinemageObject,
|
||||
to: PluginStateObject.Format.Json,
|
||||
params: (a) => {
|
||||
const kinemages = a?.data?.kinemages || [];
|
||||
const options = kinemages.map((k: Kinemage, i: number) => [i, k.pdbfile || k.caption || `Kinemage ${i}`] as const);
|
||||
return {
|
||||
index: PD.Select(0, options, { description: 'Which kinemage to use' })
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Select Kinemage', async ctx => {
|
||||
const kinData = a.data.kinemages[params.index];
|
||||
if (!kinData) {
|
||||
throw new Error(`No kinemage found at index ${params.index}`);
|
||||
}
|
||||
|
||||
const label = kinData.pdbfile || kinData.caption || `Kinemage ${params.index}`;
|
||||
|
||||
// Store the kinemage data in a Format.Json node so downstream transforms can access it
|
||||
return new PluginStateObject.Format.Json(
|
||||
{ kinData },
|
||||
{ label, description: kinData.text || '' }
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapePointsProvider = Transform({
|
||||
name: 'sb-kinemage-shape-points-provider',
|
||||
display: { name: 'Kinemage Shape Points Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Points Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapePointsFromKin(kinData, { transforms: undefined }, 'Dots').runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Points',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeLinesProvider = Transform({
|
||||
name: 'sb-kinemage-shape-lines-provider',
|
||||
display: { name: 'Kinemage Shape Lines Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Lines Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeLinesFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Lines',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeMeshProvider = Transform({
|
||||
name: 'sb-kinemage-shape-mesh-provider',
|
||||
display: { name: 'Kinemage Shape Mesh Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Mesh Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeMeshFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Meshes',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageShapeSpheresProvider = Transform({
|
||||
name: 'sb-kinemage-shape-spheres-provider',
|
||||
display: { name: 'Kinemage Shape Spheres Provider' },
|
||||
from: PluginStateObject.Format.Json,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: {}
|
||||
})({
|
||||
apply({ a }) {
|
||||
return Task.create('Kinemage Spheres Shape Provider', async ctx => {
|
||||
const kinData = (a.data as any).kinData as Kinemage;
|
||||
if (!kinData) {
|
||||
throw new Error('No kinData found in parent Format.Json node');
|
||||
}
|
||||
|
||||
const provider = await shapeSpheresFromKin(kinData).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Provider(provider as any, {
|
||||
label: kinData.pdbfile || kinData.caption || 'Kinemage Spheres',
|
||||
description: kinData.text || ''
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const KinemageExtension = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'kinemage-data-prop',
|
||||
category: 'custom-props',
|
||||
display: {
|
||||
name: 'Kinemage data',
|
||||
description: 'Data loaded from Kinemage.'
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
private provider = KinemageDataProvider;
|
||||
|
||||
register(): void {
|
||||
DefaultQueryRuntimeTable.addCustomProp(this.provider.descriptor);
|
||||
|
||||
this.ctx.customStructureProperties.register(this.provider, this.params.autoAttach);
|
||||
|
||||
// Register right-panel controls for Kinemage (show in the right-hand inspector)
|
||||
this.ctx.customStructureControls.set(Tag.Representation, KinemageControls as any);
|
||||
// Some app hosts expose a global customControls registry; register there too so the card is visible
|
||||
// even when no structure is loaded. Use `any` guards to avoid type errors if customControls isn't present.
|
||||
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.set === 'function') {
|
||||
(this.ctx as any).customControls.set('kinemage', KinemageControls as any);
|
||||
}
|
||||
|
||||
this.ctx.managers.dragAndDrop.addHandler(KinemageDragAndDropHandler.name, KinemageDragAndDropHandler.handle);
|
||||
|
||||
// Register .kin file handler so opening/dropping .kin is supported via the data formats system
|
||||
this.ctx.dataFormats.add('KIN', KINFormatProvider);
|
||||
}
|
||||
|
||||
update(p: { autoAttach: boolean }) {
|
||||
const updated = this.params.autoAttach !== p.autoAttach;
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
this.ctx.customStructureProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
|
||||
return updated;
|
||||
}
|
||||
|
||||
unregister() {
|
||||
DefaultQueryRuntimeTable.removeCustomProp(this.provider.descriptor);
|
||||
|
||||
this.ctx.customStructureProperties.unregister(this.provider.descriptor.name);
|
||||
|
||||
this.ctx.genericRepresentationControls.delete(Tag.Representation);
|
||||
|
||||
this.ctx.managers.dragAndDrop.removeHandler(KinemageDragAndDropHandler.name);
|
||||
|
||||
// Unregister the .kin data format provider
|
||||
this.ctx.dataFormats.remove('KIN');
|
||||
|
||||
// Remove right-panel controls
|
||||
try { this.ctx.customStructureControls.delete(Tag.Representation); } catch { }
|
||||
if ((this.ctx as any).customControls && typeof (this.ctx as any).customControls.delete === 'function') {
|
||||
try { (this.ctx as any).customControls.delete('kinemage'); } catch { }
|
||||
}
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(false)
|
||||
})
|
||||
});
|
||||
|
||||
/** Registerable method for handling dragged-and-dropped files */
|
||||
interface DragAndDropHandler {
|
||||
name: string,
|
||||
handle: PluginDragAndDropHandler,
|
||||
}
|
||||
|
||||
/** Helper function to create all shapes for a kinemage via proper transform chain */
|
||||
async function createShapesForKinemage(plugin: PluginContext, update: StateBuilder.Root, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
const kinDataCell = plugin.state.data.cells.get(kinDataSelector.ref);
|
||||
if (!kinDataCell?.obj?.data) return;
|
||||
|
||||
const kinData = (kinDataCell.obj.data as any).kinData as Kinemage;
|
||||
if (!kinData) return;
|
||||
|
||||
// Generate all shape types that have data, each as child of the selected kinemage
|
||||
if (kinData.dotLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapePointsProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.vectorLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeLinesProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
if (kinData.ribbonLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeMeshProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D, { doubleSided: true });
|
||||
}
|
||||
if (kinData.ballLists.length > 0) {
|
||||
await update
|
||||
.to(kinDataSelector)
|
||||
.apply(KinemageShapeSpheresProvider, {}, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
}
|
||||
}
|
||||
|
||||
/** Helper function to rebuild shapes for a kinemage (remove and recreate) */
|
||||
export async function rebuildShapesForKinemage(plugin: PluginContext, kinDataSelector: StateObjectSelector<PluginStateObject.Format.Json>) {
|
||||
// Store current camera snapshot
|
||||
const curSnap = (plugin.canvas3d && (plugin.canvas3d as any).camera && (plugin.canvas3d as any).camera.getSnapshot)
|
||||
? (plugin.canvas3d as any).camera.getSnapshot()
|
||||
: undefined;
|
||||
|
||||
const update = plugin.state.data.build();
|
||||
|
||||
// Remove all children of this kinemage node (shapes/representations)
|
||||
const children = plugin.state.data.tree.children.get(kinDataSelector.ref);
|
||||
if (children) {
|
||||
for (const childRef of children.values()) {
|
||||
update.delete(childRef);
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate shapes
|
||||
await createShapesForKinemage(plugin, update, kinDataSelector);
|
||||
await update.commit();
|
||||
|
||||
// Restore camera
|
||||
if (curSnap) {
|
||||
try {
|
||||
await applyViewSnapshot(plugin, curSnap);
|
||||
} catch (e) {
|
||||
console.warn('Failed to restore camera snapshot after recreating shapes', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Centralized helper to apply kinemage content into plugin state */
|
||||
async function applyKinemageToState(plugin: PluginContext, data: string, label?: string) {
|
||||
const update = plugin.state.data.build();
|
||||
|
||||
// Create String data node
|
||||
const dataNode = update
|
||||
.toRoot()
|
||||
.apply(StateTransforms.Data.RawData, { data, label: label || 'Kinemage File' });
|
||||
|
||||
// Parse into KinemageObject
|
||||
const parsedNode = dataNode
|
||||
.apply(ParseKinemage, { label });
|
||||
|
||||
// Select first kinemage (default)
|
||||
const selectedNode = parsedNode
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await update.commit();
|
||||
|
||||
// Now create shapes from the selected kinemage
|
||||
const shapeUpdate = plugin.state.data.build();
|
||||
await createShapesForKinemage(plugin, shapeUpdate, selectedNode.selector);
|
||||
await shapeUpdate.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const bs = getPluginBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0) return bs;
|
||||
} catch { /* ignore */ }
|
||||
await new Promise<void>(r => setTimeout(r, pollMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const bs = await waitForNonEmptyBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0 && plugin.canvas3d) {
|
||||
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
|
||||
plugin.canvas3d?.commit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to apply initial kinemage view snapshot', e);
|
||||
}
|
||||
|
||||
return selectedNode.selector;
|
||||
}
|
||||
|
||||
/** Programmatic loader: load a single File (a .kin) into the plugin state.
|
||||
* Returns the ref to the selected kinemage node.
|
||||
*/
|
||||
export async function loadKinemageFile(plugin: PluginContext, file: File): Promise<StateObjectSelector<PluginStateObject.Format.Json> | undefined> {
|
||||
const content = await file.text();
|
||||
return await applyKinemageToState(plugin, content, file.name);
|
||||
}
|
||||
|
||||
/** DragAndDropHandler handler for `.kin` files */
|
||||
const KinemageDragAndDropHandler: DragAndDropHandler = {
|
||||
name: 'kin',
|
||||
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
|
||||
let applied = false;
|
||||
for (const file of files) {
|
||||
if (file.name.toLowerCase().endsWith('.kin')) {
|
||||
const ref = await loadKinemageFile(plugin, file);
|
||||
applied = applied || !!ref;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
},
|
||||
};
|
||||
|
||||
const KINFormatProvider: DataFormatProvider<{}, any, any> = DataFormatProvider({
|
||||
label: 'KIN',
|
||||
description: 'Kinemage',
|
||||
category: 'Miscellaneous',
|
||||
stringExtensions: ['kin', 'KIN'],
|
||||
parse: async (plugin, data) => {
|
||||
try {
|
||||
// data is already a StateObjectRef to the raw data in the tree
|
||||
// Build the transform chain from it
|
||||
const builder = plugin.state.data.build()
|
||||
.to(data)
|
||||
.apply(ParseKinemage, {});
|
||||
|
||||
const selectedKin = builder
|
||||
.apply(SelectKinemage, { index: 0 });
|
||||
|
||||
await builder.commit();
|
||||
|
||||
// Return the selector for the selected kinemage so visuals can use it
|
||||
return { selectedKin: selectedKin.selector };
|
||||
} catch (e) {
|
||||
console.error('Failed to parse KIN file', e);
|
||||
throw e;
|
||||
}
|
||||
},
|
||||
visuals: async (plugin, data) => {
|
||||
if (!data?.selectedKin) {
|
||||
console.warn('[Kinemage] visuals: no selectedKin ref provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create shapes from the selected kinemage
|
||||
const shapeBuilder = plugin.state.data.build();
|
||||
await createShapesForKinemage(plugin, shapeBuilder, data.selectedKin);
|
||||
await shapeBuilder.commit();
|
||||
|
||||
// Wait for bounding sphere and focus camera
|
||||
async function waitForNonEmptyBoundingSphere(plugin: PluginContext, timeoutMs = 2000, pollMs = 50) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const bs = getPluginBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0) return bs;
|
||||
} catch { /* ignore */ }
|
||||
await new Promise<void>(r => setTimeout(r, pollMs));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const bs = await waitForNonEmptyBoundingSphere(plugin);
|
||||
if (bs && bs.radius > 0 && plugin.canvas3d) {
|
||||
await PluginCommands.Camera.Focus(plugin, { center: bs.center, radius: bs.radius, durationMs: 250 });
|
||||
plugin.canvas3d?.commit();
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to focus camera on kinemage', e);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
584
src/extensions/kinemage/kin.ts
Normal file
584
src/extensions/kinemage/kin.ts
Normal file
@@ -0,0 +1,584 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { ShapeProvider } from '../../mol-model/shape/provider';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { Kinemage, DotList, VectorList, RibbonObject, BallList } from './reader/schema';
|
||||
import { Lines } from '../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../mol-geo/geometry/lines/lines-builder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Points } from '../../mol-geo/geometry/points/points';
|
||||
import { PointsBuilder } from '../../mol-geo/geometry/points/points-builder';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
|
||||
import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
export type KinData = {
|
||||
source: Kinemage,
|
||||
transforms?: Mat4[],
|
||||
}
|
||||
|
||||
function createKinShapePointsParams(kinemage?: Kinemage) {
|
||||
return {
|
||||
...Points.Params,
|
||||
};
|
||||
}
|
||||
export const KinShapePointsParams = createKinShapePointsParams();
|
||||
export type KinShapePointsParams = typeof KinShapePointsParams
|
||||
function createKinShapeLinesParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Lines.Params,
|
||||
};
|
||||
}
|
||||
export const KinShapeLinesParams = createKinShapeLinesParams();
|
||||
export type KinShapeLinesParams = typeof KinShapeLinesParams
|
||||
function createKinShapeMeshParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Mesh.Params,
|
||||
// transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque'] as const)),
|
||||
// doubleSided: PD.Boolean(true), // make mesh double-sided by default
|
||||
// ignoreLight: PD.Boolean(true), // ignore lighting so front/back show same color
|
||||
};
|
||||
}
|
||||
|
||||
export const KinShapeMeshParams = createKinShapeMeshParams();
|
||||
export type KinShapeMeshParams = typeof KinShapeMeshParams
|
||||
|
||||
function createKinShapeSpheresParams(kinemage?: Kinemage) {
|
||||
|
||||
return {
|
||||
...Spheres.Params,
|
||||
};
|
||||
}
|
||||
|
||||
export const KinShapeSpheresParams = createKinShapeSpheresParams();
|
||||
export type KinShapeSpheresParams = typeof KinShapeSpheresParams;
|
||||
|
||||
function getVisibility(group: string, subGroup: string, masters: string[], kin: Kinemage) {
|
||||
let visible = true;
|
||||
|
||||
// Check to see if this name references a master that is not visible. If so, then this whole list is not visible and we can skip it.
|
||||
const masterDict = kin.masterDict;
|
||||
for (let m = 0; m < masters.length; m++) {
|
||||
const masterName = masters[m];
|
||||
const masterInfo = masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
visible = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check to see if this name references a group that has the 'off' flag set. If so, this is not visible.
|
||||
const groupDict = kin.groupDict;
|
||||
const groupInfo = groupDict[group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
// Check to see if this name references a subgroup that it or its master has the 'off' flag set. If so, this is not visible.
|
||||
const subgroupDict = kin.subgroupDict;
|
||||
const subgroupInfo = subgroupDict[subGroup];
|
||||
if (subgroupInfo) {
|
||||
if (subgroupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
if (subgroupInfo.group) {
|
||||
const groupInfo = groupDict[subgroupInfo.group];
|
||||
if (groupInfo && groupInfo.off) {
|
||||
visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function getPoints(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const dotLists: DotList[] = kin.dotLists;
|
||||
const builderState = PointsBuilder.create();
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every dot is in its own Molstar group because they may have colors and we look that up by group.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < dotLists.length; i++) {
|
||||
const dotList = dotLists[i];
|
||||
const positionArray = dotList.positionArray;
|
||||
const colorArray = dotList.colorArray;
|
||||
const labelArray = dotList.labelArray;
|
||||
const masterArray = dotList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this dot list if any of them are not visible.
|
||||
const visible = getVisibility(dotList.group, dotList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numDots = positionArray.length / 3;
|
||||
for (let j = 0; j < numDots; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = dotList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const group = index++;
|
||||
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(labelArray && labelArray.length > j ? labelArray[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const points = builderState.getPoints();
|
||||
return { points, colors, labels };
|
||||
}
|
||||
|
||||
async function getLines(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const vectorLists: VectorList[] = kin.vectorLists;
|
||||
const builderState = LinesBuilder.create();
|
||||
const widths: number[] = [];
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every line is in its own Molstar group because they may have individual widths and we look
|
||||
// up the width based on the group is in the size function.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < vectorLists.length; i++) {
|
||||
const vectorList = vectorLists[i];
|
||||
const position1Array = vectorList.position1Array;
|
||||
const position2Array = vectorList.position2Array;
|
||||
const widthArray = vectorList.width;
|
||||
const color1Array = vectorList.color1Array;
|
||||
const color2Array = vectorList.color2Array;
|
||||
const label1Array = vectorList.label1Array;
|
||||
const label2Array = vectorList.label2Array;
|
||||
const masterArray = vectorList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this vector list if any of them are not visible.
|
||||
const visible = getVisibility(vectorList.group, vectorList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numLines = position1Array.length / 3;
|
||||
for (let j = 0; j < numLines; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = vectorList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
// Find the midpoint of the line because we're going to actually make
|
||||
// two half-lines so that labels and selection work better.
|
||||
const midX = (position1Array[3 * j + 0] + position2Array[3 * j + 0]) / 2;
|
||||
const midY = (position1Array[3 * j + 1] + position2Array[3 * j + 1]) / 2;
|
||||
const midZ = (position1Array[3 * j + 2] + position2Array[3 * j + 2]) / 2;
|
||||
|
||||
// Make the first half of the line from position1 to the midpoint, labeled and colored based on position1.
|
||||
let group = index++;
|
||||
builderState.add(position1Array[3 * j + 0], position1Array[3 * j + 1], position1Array[3 * j + 2],
|
||||
midX, midY, midZ,
|
||||
group);
|
||||
// widthArray may be undefined; push NaN when width not provided
|
||||
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(color1Array && color1Array.length > j ? color1Array[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(label1Array && label1Array.length > j ? label1Array[j] : '');
|
||||
|
||||
// Make the second half of the line from the midpoint to position2, labeled and colored based on position2.
|
||||
group = index++;
|
||||
builderState.add(midX, midY, midZ,
|
||||
position2Array[3 * j + 0], position2Array[3 * j + 1], position2Array[3 * j + 2],
|
||||
group);
|
||||
// widthArray may be undefined; push NaN when width not provided
|
||||
widths.push(widthArray && widthArray.length > j ? widthArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(color2Array && color2Array.length > j ? color2Array[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(label2Array && label2Array.length > j ? label2Array[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const lines = builderState.getLines();
|
||||
return { lines, widths: new Float32Array(widths), colors, labels };
|
||||
}
|
||||
|
||||
function addOffsetTriangle(builderState: MeshBuilder.State, a: Vec3, b: Vec3, c: Vec3, n: Vec3, offset: number) {
|
||||
const aOffset = Vec3.add(Vec3(), a, Vec3.scale(Vec3(), n, offset));
|
||||
const bOffset = Vec3.add(Vec3(), b, Vec3.scale(Vec3(), n, offset));
|
||||
const cOffset = Vec3.add(Vec3(), c, Vec3.scale(Vec3(), n, offset));
|
||||
MeshBuilder.addTriangleWithNormal(builderState, aOffset, bOffset, cOffset, n);
|
||||
}
|
||||
|
||||
async function getMesh(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const ribbonObjects: RibbonObject[] = kin.ribbonLists;
|
||||
const builderState = MeshBuilder.createState();
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every triangle is in its own Molstar group because they may have individual colors and we look
|
||||
// up the color based on the group is in the color function.
|
||||
let index = 0;
|
||||
|
||||
for (let ri = 0; ri < ribbonObjects.length; ri++) {
|
||||
const ribbonObject = ribbonObjects[ri];
|
||||
const coords = ribbonObject.positionArray;
|
||||
const colorArray = ribbonObject.colorArray;
|
||||
const labelArray = ribbonObject.labelArray;
|
||||
const masterArray = ribbonObject.masterArray;
|
||||
const pointMasterArray = ribbonObject.pointmasterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ribbon object if any of them are not visible.
|
||||
const visible = getVisibility(ribbonObject.group, ribbonObject.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
builderState.currentGroup = ri; // TODO: Base this on something in the file instead?
|
||||
|
||||
// The positionArray contains 3x as many entries as there are vertices since it's a catenation of x, y, z for each vertex.
|
||||
// There are three vertices per triangle.
|
||||
// TODO: Ribbon lighting is to be set up to make each pair of triangles look like a quad with the same normal.
|
||||
const numTriangles = coords.length / 9;
|
||||
let prevTriangleNormal: Vec3 | undefined = undefined;
|
||||
for (let i = 0; i < numTriangles; i++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = pointMasterArray[3 * i];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const vertexList: Vec3[] = [];
|
||||
|
||||
// Get the vertices for the triangle out of the position array and push them onto a list.
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const v = Vec3.zero();
|
||||
v[0] = coords[3 * (3 * i + j) + 0];
|
||||
v[1] = coords[3 * (3 * i + j) + 1];
|
||||
v[2] = coords[3 * (3 * i + j) + 2];
|
||||
vertexList.push(v);
|
||||
}
|
||||
|
||||
// Set the group per triangle so that we can do per-triangle coloring.
|
||||
const group = index++;
|
||||
builderState.currentGroup = group;
|
||||
|
||||
// colorArray may be undefined; push a default color when not provided.
|
||||
// There is one color per group, even if we have two triangles in this group.
|
||||
const color = colorArray && colorArray.length > i * 3 ? colorArray[3 * i] : Color.fromRgb(255, 255, 255);
|
||||
colors.push(color);
|
||||
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
const label = labelArray && labelArray.length > i ? labelArray[i] : '';
|
||||
labels.push(label);
|
||||
|
||||
// Find the vertics and normal for the triangle.
|
||||
const a: Vec3 = vertexList[0];
|
||||
const b: Vec3 = vertexList[1];
|
||||
const c: Vec3 = vertexList[2];
|
||||
|
||||
// Put both orientations of the triangle. Add a small amount along the normal to make them
|
||||
// not be exactly on top of each other so that we only see the front face of each.
|
||||
let n = Vec3.zero();
|
||||
Vec3.triangleNormal(n, a, b, c);
|
||||
if (i % 2 === 1) {
|
||||
// For ribbons, every other triangle is meant to be paired with the previous one to make a quad with the same normal.
|
||||
// So use the same normal for every other triangle.
|
||||
n = prevTriangleNormal || n;
|
||||
}
|
||||
prevTriangleNormal = n;
|
||||
addOffsetTriangle(builderState, a, b, c, n, 0.01);
|
||||
|
||||
// Invert the normal for the back face.
|
||||
Vec3.negate(n, n);
|
||||
addOffsetTriangle(builderState, a, c, b, n, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(builderState);
|
||||
return { mesh, colors, labels };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build spheres geometry and collect per-sphere radii from the KIN BallList entries.
|
||||
* Returns an object with the Spheres geometry and a Float32Array with per-center radii (one entry per center, in the same order they were added).
|
||||
*/
|
||||
async function getSpheres(ctx: RuntimeContext, kin: Kinemage) {
|
||||
const balls: BallList[] = kin.ballLists;
|
||||
const builderState = SpheresBuilder.create();
|
||||
const radii: number[] = [];
|
||||
const colors: Color[] = [];
|
||||
const labels: string[] = [];
|
||||
|
||||
// Every ball is in its own Molstar group because they may have individual radii and we look
|
||||
// up the radius based on the group is in the size function.
|
||||
let index = 0;
|
||||
|
||||
for (let i = 0; i < balls.length; i++) {
|
||||
const ballList = balls[i];
|
||||
const positionArray = ballList.positionArray;
|
||||
const radiusArray = ballList.radiusArray;
|
||||
const colorArray = ballList.colorArray;
|
||||
const masterArray = ballList.masterArray;
|
||||
|
||||
// Check the visibility of all of our masters and skip this ball list if any of them are not visible.
|
||||
const visible = getVisibility(ballList.group, ballList.subgroup, masterArray, kin);
|
||||
if (!visible) { continue; }
|
||||
|
||||
const numBalls = positionArray.length / 3;
|
||||
for (let j = 0; j < numBalls; j++) {
|
||||
// Skip this element if any master associated with any of its pointMasters are turned off.
|
||||
const pointMasterNames = ballList.pointmasterArray[j];
|
||||
let pmVisibility = true;
|
||||
for (let pm = 0; pm < pointMasterNames.length; pm++) {
|
||||
const pointMasterName = pointMasterNames[pm];
|
||||
const masterName = kin.pointmasterDict[pointMasterName];
|
||||
const masterInfo = kin.masterDict[masterName];
|
||||
if (masterInfo && !masterInfo.visible) {
|
||||
pmVisibility = false;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (!pmVisibility) { continue; }
|
||||
|
||||
const group = index++;
|
||||
builderState.add(positionArray[3 * j + 0], positionArray[3 * j + 1], positionArray[3 * j + 2], group);
|
||||
// radiusArray may be undefined; push NaN when radius not provided
|
||||
radii.push(radiusArray && radiusArray.length > j ? radiusArray[j] : NaN);
|
||||
// colorArray may be undefined; push a default color when not provided
|
||||
colors.push(colorArray && colorArray.length > j ? colorArray[j] : Color.fromRgb(255, 255, 255));
|
||||
// labelArray may be undefined; push an empty string when not provided
|
||||
labels.push(ballList.labelArray && ballList.labelArray.length > j ? ballList.labelArray[j] : '');
|
||||
}
|
||||
}
|
||||
|
||||
const spheres = builderState.getSpheres();
|
||||
return { spheres, radii: new Float32Array(radii), colors, labels };
|
||||
}
|
||||
|
||||
function makePointsShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapePointsParams>, shape?: Shape<Points>) => {
|
||||
// Get our points, adding them from all of the entries in the dot lists
|
||||
const { points: _points, colors, labels } = await getPoints(ctx, kinData.source);
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Points>(
|
||||
'kin-points',
|
||||
kinData.source,
|
||||
_points,
|
||||
colorFn, // color function reads per-point colors
|
||||
() => 1, // size function
|
||||
labelFn // label function reads per-point labels
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeLineShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeLinesParams>, shape?: Shape<Lines>) => {
|
||||
// Get our lines, adding them from all of the entries in the vector lists
|
||||
const { lines: _lines, widths, colors, labels } = await getLines(ctx, kinData.source);
|
||||
|
||||
// Size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const sizeFn = (group: number, instance: number) => {
|
||||
// We're specifying the radius, which is half the width.
|
||||
let w = widths[group] / 2.0;
|
||||
if (w < 1.0) { w = 1.0; }
|
||||
return Number.isFinite(w) ? w : 1.0;
|
||||
};
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Lines>(
|
||||
'kin-lines',
|
||||
kinData.source,
|
||||
_lines,
|
||||
colorFn, // color function reads per-line colors
|
||||
sizeFn, // size function reads per-line widths
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
function makeMeshShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeMeshParams>, shape?: Shape<Mesh>) => {
|
||||
|
||||
let { mesh: _mesh, colors, labels } = await getMesh(ctx, kinData.source);
|
||||
// Ensure that _mesh is not undifined before we pass it to Shape.create. If it is undefined, create an empty mesh instead.
|
||||
if (!_mesh) {
|
||||
console.warn('No mesh could be created from the KIN data. Creating an empty mesh instead.');
|
||||
_mesh = Mesh.createEmpty();
|
||||
}
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Lines the groupId corresponds to the line index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Mesh>(
|
||||
'kin-mesh',
|
||||
kinData.source,
|
||||
_mesh,
|
||||
colorFn, // color function reads per-triangle colors
|
||||
() => 1, // size function
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spheres shape getter: uses per-center radii read from the KIN BallList radiusArray when available.
|
||||
*/
|
||||
function makeSpheresShapeGetter() {
|
||||
|
||||
const getShape = async (ctx: RuntimeContext, kinData: KinData, props: PD.Values<KinShapeSpheresParams>, shape?: Shape<Spheres>) => {
|
||||
// Build spheres geometry and collect per-center radii
|
||||
const { spheres: _spheres, radii, colors, labels } = await getSpheres(ctx, kinData.source);
|
||||
|
||||
// size function signature: (groupId: number, instanceId: number) => number
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const sizeFn = (group: number, instance: number) => {
|
||||
const r = radii[group];
|
||||
return Number.isFinite(r) ? r : 1.0;
|
||||
};
|
||||
|
||||
// Color function signature: (groupId: number, instanceId: number) => Color
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const colorFn = (group: number, instance: number) => {
|
||||
return colors[group];
|
||||
};
|
||||
|
||||
// Label function signature: (groupId: number, instanceId: number) => string
|
||||
// For Spheres the groupId corresponds to the center index (order added).
|
||||
const labelFn = (group: number, instance: number) => {
|
||||
return labels[group];
|
||||
};
|
||||
|
||||
const _shape = Shape.create<Spheres>(
|
||||
'kin-spheres',
|
||||
kinData.source,
|
||||
_spheres,
|
||||
colorFn, // color function reads per-center colors
|
||||
sizeFn, // size function reads per-center radii
|
||||
labelFn // label function
|
||||
);
|
||||
return _shape;
|
||||
};
|
||||
return getShape;
|
||||
}
|
||||
|
||||
export function shapePointsFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Points, KinShapePointsParams>>('Kin Shape Points Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Points',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapePointsParams(source),
|
||||
getShape: makePointsShapeGetter(),
|
||||
geometryUtils: Points.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeLinesFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Lines, KinShapeLinesParams>>('Kin Shape Lines Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Lines',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeLinesParams(source),
|
||||
getShape: makeLineShapeGetter(),
|
||||
geometryUtils: Lines.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeMeshFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Mesh, KinShapeMeshParams>>('Kin Shape Mesh Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Meshes',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeMeshParams(source),
|
||||
getShape: makeMeshShapeGetter(),
|
||||
geometryUtils: Mesh.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function shapeSpheresFromKin(source: Kinemage, params?: { transforms?: Mat4[] }, label?: string) {
|
||||
return Task.create<ShapeProvider<KinData, Spheres, KinShapeSpheresParams>>('Kin Shape Spheres Provider', async ctx => {
|
||||
return {
|
||||
label: label ?? 'Spheres',
|
||||
data: { source, transforms: params?.transforms },
|
||||
params: createKinShapeSpheresParams(source),
|
||||
getShape: makeSpheresShapeGetter(),
|
||||
geometryUtils: Spheres.Utils
|
||||
};
|
||||
});
|
||||
}
|
||||
116
src/extensions/kinemage/prop.ts
Normal file
116
src/extensions/kinemage/prop.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { Task } from '../../mol-task';
|
||||
|
||||
import { Kinemage } from './reader/schema';
|
||||
import { parseKin } from './reader/parser';
|
||||
|
||||
export const KinemageParams = {
|
||||
};
|
||||
export type KinemageParams = typeof KinemageParams
|
||||
export type KinemageProps = PD.Values<KinemageParams>
|
||||
|
||||
export const KinemageDataParams = {
|
||||
...KinemageParams
|
||||
};
|
||||
export type KinemageDataParams = typeof KinemageDataParams
|
||||
export type KinemageDataProps = PD.Values<KinemageDataParams>
|
||||
|
||||
export { KinemageData };
|
||||
|
||||
interface KinemageData {
|
||||
/**
|
||||
* List of Kinemages read from one or more files.
|
||||
*/
|
||||
readonly kinemages: Kinemage[]
|
||||
}
|
||||
|
||||
const FileSourceParams = {
|
||||
input: PD.File({ accept: '.kin', multiple: false })
|
||||
};
|
||||
type FileSourceProps = PD.Values<typeof FileSourceParams>
|
||||
|
||||
namespace KinemageData {
|
||||
export enum Tag {
|
||||
Representation = 'kinemage-3d'
|
||||
}
|
||||
|
||||
export const symbols = {
|
||||
};
|
||||
|
||||
async function loadKinemageData(data: string): Promise<Kinemage[]> {
|
||||
const task = parseKin(data);
|
||||
const result = await task.run();
|
||||
if (result.isError) {
|
||||
throw new Error('Failed to parse KIN data');
|
||||
}
|
||||
return result.result;
|
||||
}
|
||||
|
||||
export async function open(file: FileSourceProps | File): Promise<KinemageData> {
|
||||
|
||||
let fileToRead: File;
|
||||
|
||||
if (file instanceof File) {
|
||||
fileToRead = file;
|
||||
} else if (file && file.input && file.input.file) {
|
||||
fileToRead = file.input.file;
|
||||
} else {
|
||||
throw new Error('No file given');
|
||||
}
|
||||
|
||||
const task = Task.create('Load KIN file', async ctx => {
|
||||
const data = await fileToRead.text();
|
||||
const kinemages = await loadKinemageData(data);
|
||||
return kinemages;
|
||||
});
|
||||
|
||||
const kinemages = await task.run();
|
||||
return { kinemages };
|
||||
}
|
||||
}
|
||||
|
||||
export const KinemageDataProvider: CustomStructureProperty.Provider<KinemageDataParams, KinemageData> = CustomStructureProperty.createProvider({
|
||||
label: 'Kinemage',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'Kinemage_loaded_data',
|
||||
symbols: KinemageData.symbols,
|
||||
}),
|
||||
type: 'root',
|
||||
defaultParams: KinemageDataParams,
|
||||
getParams: (data: Structure) => KinemageDataParams,
|
||||
isApplicable,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<KinemageDataProps>) => {
|
||||
const p = { ...PD.getDefaultValues(KinemageDataParams), ...props };
|
||||
try {
|
||||
return { value: await computeKinemageProps(ctx, data, p) };
|
||||
} catch (e) {
|
||||
// the "Residues Embedded in Membrane" symbol may bypass isApplicable() checks
|
||||
console.warn('Failed to predict membrane orientation. This happens for short peptides and entries without amino acids.');
|
||||
return { value: undefined };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function isApplicable(structure: Structure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function computeKinemageProps(ctx: CustomProperty.Context, data: Structure, props: Partial<KinemageProps>): Promise<KinemageData> {
|
||||
// Return an empty KinemageData object since the actual data will be loaded asynchronously via the `open` method.
|
||||
// This allows the property to be attached to the structure without blocking on file loading.
|
||||
return {
|
||||
kinemages: []
|
||||
};
|
||||
}
|
||||
944
src/extensions/kinemage/reader/kinparser.ts
Normal file
944
src/extensions/kinemage/reader/kinparser.ts
Normal file
@@ -0,0 +1,944 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Based on earlier kin-parser.ts file from the NGL project (see second author notice below).
|
||||
* @file Ported NGL-based Kinemage file parser
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
* @private
|
||||
*/
|
||||
|
||||
/**
|
||||
* file Kin Parser
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { Kinemage, RibbonObject } from './schema';
|
||||
import { Hsv } from '../../../mol-util/color/spaces/hsv';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
const ColorDict: { [k: string]: Color } = {
|
||||
red: Hsv.toColor(Hsv.fromArray([0, 100, 100])),
|
||||
orange: Hsv.toColor(Hsv.fromArray([20, 100, 100])),
|
||||
gold: Hsv.toColor(Hsv.fromArray([40, 100, 100])),
|
||||
yellow: Hsv.toColor(Hsv.fromArray([60, 100, 100])),
|
||||
lime: Hsv.toColor(Hsv.fromArray([80, 100, 100])),
|
||||
green: Hsv.toColor(Hsv.fromArray([120, 80, 100])),
|
||||
sea: Hsv.toColor(Hsv.fromArray([150, 100, 100])),
|
||||
cyan: Hsv.toColor(Hsv.fromArray([180, 100, 85])),
|
||||
sky: Hsv.toColor(Hsv.fromArray([210, 75, 95])),
|
||||
blue: Hsv.toColor(Hsv.fromArray([240, 70, 100])),
|
||||
purple: Hsv.toColor(Hsv.fromArray([275, 75, 100])),
|
||||
magenta: Hsv.toColor(Hsv.fromArray([300, 95, 100])),
|
||||
hotpink: Hsv.toColor(Hsv.fromArray([335, 100, 100])),
|
||||
pink: Hsv.toColor(Hsv.fromArray([350, 55, 100])),
|
||||
peach: Hsv.toColor(Hsv.fromArray([25, 75, 100])),
|
||||
lilac: Hsv.toColor(Hsv.fromArray([275, 55, 100])),
|
||||
pinktint: Hsv.toColor(Hsv.fromArray([340, 30, 100])),
|
||||
peachtint: Hsv.toColor(Hsv.fromArray([25, 50, 100])),
|
||||
yellowtint: Hsv.toColor(Hsv.fromArray([60, 50, 100])),
|
||||
greentint: Hsv.toColor(Hsv.fromArray([135, 40, 100])),
|
||||
bluetint: Hsv.toColor(Hsv.fromArray([220, 40, 100])),
|
||||
lilactint: Hsv.toColor(Hsv.fromArray([275, 35, 100])),
|
||||
white: Hsv.toColor(Hsv.fromArray([0, 0, 100])),
|
||||
gray: Hsv.toColor(Hsv.fromArray([0, 0, 50])),
|
||||
brown: Hsv.toColor(Hsv.fromArray([20, 45, 75])),
|
||||
deadwhite: Hsv.toColor(Hsv.fromArray([0, 0, 100])),
|
||||
deadblack: Hsv.toColor(Hsv.fromArray([0, 0, 0])),
|
||||
invisible: Hsv.toColor(Hsv.fromArray([0, 0, 0]))
|
||||
};
|
||||
|
||||
const reWhitespaceComma = /[\s,]+/;
|
||||
const reCurlyWhitespace = /[^{}\s]*{[^{}]+}|[^{}\s]+/g;
|
||||
const reTrimCurly = /^{+|}+$/g;
|
||||
const reCollapseEqual = /\s*=\s*/g;
|
||||
|
||||
function parseListDef(line: string, localColorDict: { [k: string]: Color }) {
|
||||
let name;
|
||||
let defaultColor: Color = localColorDict['white']; // Default color is white, but it can be overridden by the list definition
|
||||
let radius;
|
||||
let nobutton = false;
|
||||
const master = [];
|
||||
let width = 2; // Default width is 2, but it can be overridden by the list definition
|
||||
|
||||
line = line.replace(reCollapseEqual, '=');
|
||||
|
||||
const lm = line.match(reCurlyWhitespace) as RegExpMatchArray;
|
||||
for (let j = 1; j < lm.length; ++j) {
|
||||
const e = lm[j];
|
||||
if (e[0] === '{') {
|
||||
name = e.substring(1, e.length - 1);
|
||||
} else {
|
||||
const es = e.split('=');
|
||||
if (es.length === 2) {
|
||||
if (es[0] === 'color') {
|
||||
const colorName = parseStr(es[1]);
|
||||
defaultColor = localColorDict[colorName];
|
||||
} else if (es[0] === 'width') {
|
||||
width = parseInt(es[1]);
|
||||
} else if (es[0] === 'master') {
|
||||
master.push(es[1].replace(reTrimCurly, ''));
|
||||
} else if (es[0] === 'radius') {
|
||||
radius = parseFloat(es[1]);
|
||||
} else {
|
||||
console.log('Kinemage: Unknown list definition term found: ' + es[0]);
|
||||
}
|
||||
} else if (e === 'nobutton') {
|
||||
nobutton = true;
|
||||
} else {
|
||||
console.log('Kinemage: Unknown list definition term found: ' + e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
listName: name,
|
||||
listColor: defaultColor,
|
||||
listMasters: master,
|
||||
listWidth: width,
|
||||
listRadius: radius,
|
||||
nobutton: nobutton
|
||||
};
|
||||
}
|
||||
|
||||
function parseListElm(line: string, localColorDict: { [k: string]: Color }) {
|
||||
line = line.trim();
|
||||
|
||||
const idx1 = line.indexOf('{');
|
||||
const idx2 = line.indexOf('}');
|
||||
const ls = line.substr(idx2 + 1).split(reWhitespaceComma);
|
||||
|
||||
const label = line.substr(idx1 + 1, idx2 - 1);
|
||||
const position = [
|
||||
parseFloat(ls[ls.length - 3]),
|
||||
parseFloat(ls[ls.length - 2]),
|
||||
parseFloat(ls[ls.length - 1])
|
||||
];
|
||||
let color, width, radius;
|
||||
let lineBreak = false;
|
||||
let triangleBreak = false;
|
||||
const pointMasters: string[] = [];
|
||||
for (let lsindex = 4; lsindex <= ls.length; lsindex++) {
|
||||
const literal = ls[ls.length - lsindex];
|
||||
if (literal in localColorDict) {
|
||||
color = localColorDict[ls[ls.length - lsindex]];
|
||||
}
|
||||
if (literal.startsWith('width')) {
|
||||
width = parseInt(literal.substring(5));
|
||||
}
|
||||
if (literal.startsWith('r=')) {
|
||||
radius = parseFloat(literal.split('=')[1]);
|
||||
}
|
||||
if (literal.startsWith('P')) {
|
||||
lineBreak = true;
|
||||
}
|
||||
if (literal.startsWith('X')) {
|
||||
triangleBreak = true;
|
||||
}
|
||||
if (literal.startsWith("'") && literal.endsWith("'")) {
|
||||
// Handle single-character tags by putting each character into a pointMaster tag, e.g. 'ab' would be two tags, 'a' and 'b'
|
||||
const tagString: string = literal.substring(1, literal.length - 1);
|
||||
for (let i = 0; i < tagString.length; i++) {
|
||||
pointMasters.push(tagString[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: label,
|
||||
position: position,
|
||||
color: color,
|
||||
radius: radius,
|
||||
width: width,
|
||||
isLineBreak: lineBreak,
|
||||
isTriangleBreak: triangleBreak,
|
||||
pointMasters: pointMasters
|
||||
};
|
||||
}
|
||||
|
||||
function parseStr(line: string) {
|
||||
const start = line.indexOf('{');
|
||||
const end = line.indexOf('}');
|
||||
return line.substring(
|
||||
start !== -1 ? start + 1 : 0,
|
||||
end !== -1 ? end : undefined
|
||||
).trim();
|
||||
}
|
||||
|
||||
function parseFlag(line: string) {
|
||||
const end = line.indexOf('}');
|
||||
return end === -1 ? undefined : line.substr(end + 1).trim();
|
||||
}
|
||||
|
||||
function parseGroup(line: string) {
|
||||
let name: string = '';
|
||||
const master: string[] = [];
|
||||
const flags: { [k: string]: string | boolean } = {};
|
||||
|
||||
line = line.replace(reCollapseEqual, '=');
|
||||
|
||||
const lm = line.match(reCurlyWhitespace) as RegExpMatchArray;
|
||||
for (let j = 1; j < lm.length; ++j) {
|
||||
const e = lm[j];
|
||||
if (e[0] === '{') {
|
||||
name = e.substring(1, e.length - 1);
|
||||
} else {
|
||||
const es = e.split('=');
|
||||
if (es.length === 2) {
|
||||
if (es[0] === 'master') {
|
||||
master.push(es[1].replace(reTrimCurly, ''));
|
||||
} else {
|
||||
flags[es[0]] = es[1].replace(reTrimCurly, '');
|
||||
}
|
||||
} else {
|
||||
flags[es[0]] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
groupName: name,
|
||||
groupFlags: flags,
|
||||
groupMasters: master,
|
||||
};
|
||||
}
|
||||
|
||||
function parsePointmaster(line: string) {
|
||||
let name: string = '';
|
||||
const tags: string[] = [];
|
||||
let on: boolean | undefined = undefined;
|
||||
|
||||
// Find the string name between curly braces, or print an error if not found
|
||||
const nameMatch = line.match(/{([^}]+)}/);
|
||||
if (nameMatch) {
|
||||
name = nameMatch[1];
|
||||
|
||||
// Find all characters between the pair of single quotes, which are the tags, and add them to the tags array
|
||||
const tagMatch = line.match(/'([^']+)'/);
|
||||
if (tagMatch) {
|
||||
const tagString: string = tagMatch[1];
|
||||
for (let i = 0; i < tagString.length; i++) {
|
||||
tags.push(tagString[i]);
|
||||
}
|
||||
|
||||
// See if the line contains the word "on" or "off" and set the on variable accordingly
|
||||
if (line.includes(' on')) {
|
||||
on = true;
|
||||
} else if (line.includes(' off')) {
|
||||
on = false;
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log('Kinemage: Pointmaster definition missing tags: ' + line);
|
||||
}
|
||||
} else {
|
||||
console.log('Kinemage: Pointmaster definition missing name: ' + line);
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
tags: tags,
|
||||
on: on
|
||||
};
|
||||
}
|
||||
|
||||
function convertKinTriangleArrays(ribbonObject: RibbonObject) {
|
||||
// have to convert ribbons/triangle lists from stripdrawmode to normal drawmode
|
||||
// index [ 0 1 2 3 4 5 6 7 8 91011 ]
|
||||
// label/color/ptm [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ]
|
||||
// convertedindex [ 0 1 2 3 4 5 6 7 8 91011121314151617181920212223242526 ]
|
||||
// index [ 0 1 2 3 4 5 6 7 8 91011121314 ] [ 0 1 2 3 4 5 6 7 8 3 4 5 6 7 8 91011 6 7 8 91011121314 ]
|
||||
// position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ]
|
||||
const { labelArray, positionArray, colorArray, breakArray } = ribbonObject;
|
||||
const convertedLabels = [];
|
||||
for (let i = 0; i < (labelArray.length - 2) * 3; ++i) {
|
||||
convertedLabels[i] = labelArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedColors = [];
|
||||
for (let i = 0; i < (colorArray.length - 2) * 3; ++i) {
|
||||
convertedColors[i] = colorArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedPMs = [];
|
||||
for (let i = 0; i < (ribbonObject.pointmasterArray.length - 2) * 3; ++i) {
|
||||
convertedPMs[i] = ribbonObject.pointmasterArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedBreaks = [];
|
||||
for (let i = 0; i < (breakArray.length - 2) * 3; ++i) {
|
||||
convertedBreaks[i] = breakArray[i - Math.floor(i / 3) * 2];
|
||||
}
|
||||
const convertedPositions = [];
|
||||
for (let i = 0; i < (positionArray.length / 3 - 2) * 9; ++i) {
|
||||
convertedPositions[i] = positionArray[i - Math.floor(i / 9) * 6];
|
||||
}
|
||||
const vector3Positions = [];
|
||||
for (let i = 0; i < (convertedPositions.length) / 3; ++i) {
|
||||
vector3Positions.push([convertedPositions[i * 3], convertedPositions[i * 3] + 1, convertedPositions[i * 3] + 2]);
|
||||
}
|
||||
return {
|
||||
group: ribbonObject.group,
|
||||
subgroup: ribbonObject.subgroup,
|
||||
name: ribbonObject.name,
|
||||
masterArray: ribbonObject.masterArray,
|
||||
pointmasterArray: convertedPMs,
|
||||
nobutton: ribbonObject.nobutton,
|
||||
labelArray: convertedLabels,
|
||||
positionArray: convertedPositions,
|
||||
breakArray: convertedBreaks,
|
||||
colorArray: convertedColors,
|
||||
pairTriangleNormals: ribbonObject.pairTriangleNormals
|
||||
};
|
||||
}
|
||||
|
||||
function removePointBreaksTriangleArrays(convertedRibbonObject: RibbonObject) {
|
||||
// after converting ribbon/triangle arrys to drawmode, removed point break triangles
|
||||
// label/color [ 0 1 2 3 4 5 ] to [ 0 1 2 1 2 3 2 3 4 3 4 5 ]
|
||||
// position [ 0 0 0 1 1 1 2 2 2 3 3 3 4 4 4 ] to [ 0 0 0 1 1 1 2 2 2 1 1 1 2 2 2 3 3 3 2 2 2 3 3 3 4 4 4 ]
|
||||
const { labelArray, positionArray, colorArray, breakArray } = convertedRibbonObject;
|
||||
const editedLabels = [];
|
||||
const editedPositions = [];
|
||||
const editedColors = [];
|
||||
const editedPMs = [];
|
||||
const editedBreaks = [];
|
||||
for (let i = 0; i < breakArray.length / 3; i++) {
|
||||
const breakPointer = i * 3;
|
||||
const positionPointer = i * 9;
|
||||
if (!breakArray[breakPointer + 1] && !breakArray[breakPointer + 2]) {
|
||||
editedLabels.push(labelArray[breakPointer]);
|
||||
editedLabels.push(labelArray[breakPointer + 1]);
|
||||
editedLabels.push(labelArray[breakPointer + 2]);
|
||||
editedBreaks.push(breakArray[breakPointer]);
|
||||
editedBreaks.push(breakArray[breakPointer + 1]);
|
||||
editedBreaks.push(breakArray[breakPointer + 2]);
|
||||
editedPositions.push(positionArray[positionPointer]);
|
||||
editedPositions.push(positionArray[positionPointer + 1]);
|
||||
editedPositions.push(positionArray[positionPointer + 2]);
|
||||
editedPositions.push(positionArray[positionPointer + 3]);
|
||||
editedPositions.push(positionArray[positionPointer + 4]);
|
||||
editedPositions.push(positionArray[positionPointer + 5]);
|
||||
editedPositions.push(positionArray[positionPointer + 6]);
|
||||
editedPositions.push(positionArray[positionPointer + 7]);
|
||||
editedPositions.push(positionArray[positionPointer + 8]);
|
||||
editedColors.push(colorArray[breakPointer]);
|
||||
editedColors.push(colorArray[breakPointer + 1]);
|
||||
editedColors.push(colorArray[breakPointer + 2]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 1]);
|
||||
editedPMs.push(convertedRibbonObject.pointmasterArray[breakPointer + 2]);
|
||||
} else {
|
||||
// console.log('X triangle break found')
|
||||
// console.log('skipping: '+positionArray[positionPointer]+','+positionArray[positionPointer+1]+','+positionArray[positionPointer+2]+','
|
||||
// +positionArray[positionPointer+3]+','+positionArray[positionPointer+4]+','+positionArray[positionPointer+5]+','
|
||||
// +positionArray[positionPointer+6]+','+positionArray[positionPointer+7]+','+positionArray[positionPointer+8])
|
||||
}
|
||||
}
|
||||
return {
|
||||
group: convertedRibbonObject.group,
|
||||
subgroup: convertedRibbonObject.subgroup,
|
||||
name: convertedRibbonObject.name,
|
||||
masterArray: convertedRibbonObject.masterArray,
|
||||
pointmasterArray: editedPMs,
|
||||
nobutton: convertedRibbonObject.nobutton,
|
||||
labelArray: editedLabels,
|
||||
positionArray: editedPositions,
|
||||
breakArray: editedBreaks,
|
||||
colorArray: editedColors,
|
||||
pairTriangleNormals: convertedRibbonObject.pairTriangleNormals
|
||||
};
|
||||
}
|
||||
|
||||
class KinParser {
|
||||
// @brief Property that is filled in by the constructor as it parses the file. Read by the caller.
|
||||
kinemage: Kinemage;
|
||||
|
||||
// @brief Constructor for the KinParser class.
|
||||
// @param data The string data to be parsed, including all lines in the file.
|
||||
constructor(data: string) {
|
||||
this._parse(data);
|
||||
}
|
||||
|
||||
private _parse(data: string) {
|
||||
// http://kinemage.biochem.duke.edu/software/king.php
|
||||
|
||||
const kinemage: Kinemage = {
|
||||
comments: [],
|
||||
kinemage: undefined,
|
||||
onewidth: undefined,
|
||||
viewDict: {},
|
||||
pdbfile: undefined,
|
||||
texts: [],
|
||||
text: '',
|
||||
captions: [],
|
||||
caption: '',
|
||||
groupDict: {},
|
||||
subgroupDict: {},
|
||||
masterDict: {},
|
||||
pointmasterDict: {},
|
||||
dotLists: [],
|
||||
vectorLists: [],
|
||||
ballLists: [],
|
||||
ribbonLists: [],
|
||||
groupsAnimate: [],
|
||||
activeAnimateGroup: -1,
|
||||
groupsAnimate2: [],
|
||||
activeAnimateGroup2: -1
|
||||
};
|
||||
this.kinemage = kinemage;
|
||||
|
||||
// Keep a local copy of the ColorDict that we can update with new colors defined in the file.
|
||||
const localColorDict: { [k: string]: Color } = Object.assign({}, ColorDict);
|
||||
|
||||
let currentGroup: string = '';
|
||||
let currentGroupMasters: string[];
|
||||
let currentSubgroup: string = '';
|
||||
let currentSubgroupMasters: string[];
|
||||
|
||||
let isDotList = false;
|
||||
let prevDotLabel = '';
|
||||
let dotDefaultColor: Color;
|
||||
let dotLabel: string[], dotPosition: number[], dotColor: Color[], dotPointMasters: string[][];
|
||||
|
||||
let isVectorList = false;
|
||||
let prevVecLabel = '';
|
||||
let prevVecPosition: number[] | null = null;
|
||||
let prevVecColor: Color | null = null;
|
||||
let vecDefaultColor: Color, vecDefaultWidth: number;
|
||||
let vecLabel1: string[], vecLabel2: string[], vecPosition1: number[], vecPosition2: number[], vecColor1: Color[], vecColor2: Color[];
|
||||
let vecWidth: number[], vecPointMasters: string[][];
|
||||
|
||||
let isBallList = false;
|
||||
let prevBallLabel = '';
|
||||
let ballRadius: number[], ballDefaultColor: Color, ballDefaultRadius: number;
|
||||
let ballLabel: string[], ballPosition: number[], ballColor: Color[], ballPointMasters: string[][];
|
||||
|
||||
let isRibbonList = false;
|
||||
let ribbonIsTriangles = false;
|
||||
let prevRibbonPointLabel = '';
|
||||
|
||||
let ribbonListDefaultColor: Color = localColorDict['white'];
|
||||
let ribbonPointLabelArray: string[], ribbonPointPositionArray: number[], ribbonPointBreakArray: boolean[], ribbonPointColorArray: Color[];
|
||||
let ribbonPointMasters: string[][];
|
||||
|
||||
let isText = false;
|
||||
let isCaption = false;
|
||||
|
||||
let foundAnimate = false;
|
||||
let found2Animate = false;
|
||||
|
||||
function _parseChunkOfLines(_i: number, _n: number, lines: string[]) {
|
||||
for (let i = _i; i < _n; ++i) {
|
||||
const line = lines[i];
|
||||
|
||||
if (line[0] === '@') {
|
||||
isDotList = false;
|
||||
isVectorList = false;
|
||||
isBallList = false;
|
||||
isRibbonList = false;
|
||||
isText = false;
|
||||
isCaption = false;
|
||||
}
|
||||
|
||||
if (!line) {
|
||||
isDotList = false;
|
||||
isVectorList = false;
|
||||
isBallList = false;
|
||||
isRibbonList = false;
|
||||
} else if (line.startsWith('@dot') /* dot or dotlist */) {
|
||||
// @dotlist {x} color=white master={vdw contact} master={dots}
|
||||
|
||||
let { listColor, listName, listMasters, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
isDotList = true;
|
||||
prevDotLabel = '';
|
||||
dotLabel = [];
|
||||
dotPosition = [];
|
||||
dotColor = [];
|
||||
dotPointMasters = [];
|
||||
dotDefaultColor = listColor;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.dotLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: dotPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: dotLabel,
|
||||
positionArray: dotPosition,
|
||||
colorArray: dotColor
|
||||
});
|
||||
} else if (line.startsWith('@vector') /* vector or vectorlist */) {
|
||||
// @vectorlist {x} color=white master={small overlap} master={dots}
|
||||
|
||||
let { listMasters, listName, listWidth, listColor, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isVectorList = true;
|
||||
prevVecLabel = '';
|
||||
prevVecPosition = null;
|
||||
prevVecColor = null;
|
||||
vecLabel1 = [];
|
||||
vecLabel2 = [];
|
||||
vecPosition1 = [];
|
||||
vecPosition2 = [];
|
||||
vecColor1 = [];
|
||||
vecColor2 = [];
|
||||
vecWidth = [];
|
||||
vecDefaultColor = listColor;
|
||||
vecPointMasters = [];
|
||||
vecDefaultWidth = 2;
|
||||
if (listWidth) {
|
||||
vecDefaultWidth = listWidth;
|
||||
}
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.vectorLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: vecPointMasters,
|
||||
nobutton: nobutton,
|
||||
label1Array: vecLabel1,
|
||||
label2Array: vecLabel2,
|
||||
position1Array: vecPosition1,
|
||||
position2Array: vecPosition2,
|
||||
color1Array: vecColor1,
|
||||
color2Array: vecColor2,
|
||||
width: vecWidth
|
||||
});
|
||||
} else if (line.startsWith('@ball') /* ball or balllist*/ || line.startsWith('@sphere') /* sphere or spherelist */) {
|
||||
let { listName, listColor, listMasters, listRadius, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
isBallList = true;
|
||||
|
||||
prevBallLabel = '';
|
||||
ballLabel = [];
|
||||
ballRadius = [];
|
||||
ballPosition = [];
|
||||
ballColor = [];
|
||||
ballPointMasters = [];
|
||||
ballDefaultColor = listColor;
|
||||
ballDefaultRadius = listRadius !== undefined ? listRadius : 1;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.ballLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: ballPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: ballLabel,
|
||||
radiusArray: ballRadius,
|
||||
positionArray: ballPosition,
|
||||
colorArray: ballColor
|
||||
});
|
||||
} else if (line.startsWith('@ribbon') /* ribbon or ribbonlist */ || line.startsWith('@triangle') /* triangle or trianglelist */) {
|
||||
let { listMasters, listName, listColor, nobutton } = parseListDef(line, localColorDict);
|
||||
|
||||
if (listMasters) {
|
||||
listMasters.forEach(function (name: string) {
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
isRibbonList = true;
|
||||
ribbonIsTriangles = line.startsWith('@triangle'); /* triangle or trianglelist */
|
||||
prevRibbonPointLabel = '';
|
||||
ribbonPointLabelArray = [];
|
||||
ribbonPointPositionArray = [];
|
||||
ribbonPointBreakArray = [];
|
||||
ribbonPointColorArray = [];
|
||||
ribbonListDefaultColor = listColor;
|
||||
ribbonPointMasters = [];
|
||||
|
||||
if (currentGroupMasters) {
|
||||
listMasters = listMasters.concat(currentGroupMasters);
|
||||
}
|
||||
if (currentSubgroupMasters) {
|
||||
listMasters = listMasters.concat(currentSubgroupMasters);
|
||||
}
|
||||
|
||||
kinemage.ribbonLists.push({
|
||||
group: currentGroup,
|
||||
subgroup: currentSubgroup,
|
||||
name: listName,
|
||||
masterArray: listMasters,
|
||||
pointmasterArray: ribbonPointMasters,
|
||||
nobutton: nobutton,
|
||||
labelArray: ribbonPointLabelArray,
|
||||
positionArray: ribbonPointPositionArray,
|
||||
breakArray: ribbonPointBreakArray,
|
||||
colorArray: ribbonPointColorArray,
|
||||
pairTriangleNormals: !ribbonIsTriangles
|
||||
});
|
||||
} else if (line.startsWith('@text')) {
|
||||
isText = true;
|
||||
kinemage.texts.push(line.substr(5));
|
||||
} else if (line.startsWith('@caption')) {
|
||||
isCaption = true;
|
||||
kinemage.captions.push(line.substr(8));
|
||||
} else if (isDotList) {
|
||||
// { CB THR 1 A}sky 'P' 18.915,14.199,5.024
|
||||
|
||||
let { label, color, position, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevDotLabel;
|
||||
} else {
|
||||
prevDotLabel = label;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = dotDefaultColor;
|
||||
}
|
||||
|
||||
dotLabel.push(label);
|
||||
dotPosition.push(...position);
|
||||
dotColor.push(color);
|
||||
dotPointMasters.push(pointMasters);
|
||||
} else if (isVectorList) {
|
||||
// { n thr A 1 B13.79 1crnFH} P 17.047, 14.099, 3.625 { n thr A 1 B13.79 1crnFH} L 17.047, 14.099, 3.625
|
||||
|
||||
const doubleLine = line.replace(/(?!^){/g, '\n{');
|
||||
const splitLine = doubleLine.split(/\n/);
|
||||
|
||||
for (let i2 = 0; i2 < splitLine.length; i2++) {
|
||||
const singlePointLine = splitLine[i2];
|
||||
let { label, color, width, position, isLineBreak, pointMasters } = parseListElm(singlePointLine, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevVecLabel;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = vecDefaultColor;
|
||||
}
|
||||
|
||||
if (!isLineBreak) {
|
||||
if (prevVecPosition !== null) {
|
||||
if (width === undefined) {
|
||||
width = vecDefaultWidth;
|
||||
}
|
||||
|
||||
vecLabel1.push(prevVecLabel);
|
||||
vecPosition1.push(...prevVecPosition);
|
||||
vecColor1.push(prevVecColor ? prevVecColor : vecDefaultColor);
|
||||
|
||||
vecLabel2.push(label);
|
||||
vecPosition2.push(...position);
|
||||
vecColor2.push(color);
|
||||
vecWidth.push(width);
|
||||
|
||||
vecPointMasters.push(pointMasters);
|
||||
}
|
||||
}
|
||||
|
||||
prevVecLabel = label;
|
||||
prevVecPosition = position;
|
||||
prevVecColor = color;
|
||||
}
|
||||
} else if (isBallList) {
|
||||
// {cb arg A 1 1.431 -106.80} r=1.431 39.085, 8.083, 22.182
|
||||
|
||||
let { label, radius, color, position, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevBallLabel;
|
||||
} else {
|
||||
prevBallLabel = label;
|
||||
}
|
||||
|
||||
if (radius === undefined) {
|
||||
radius = ballDefaultRadius;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = ballDefaultColor;
|
||||
}
|
||||
|
||||
ballLabel.push(label);
|
||||
ballRadius.push(radius);
|
||||
ballPosition.push(...position);
|
||||
ballColor.push(color);
|
||||
ballPointMasters.push(pointMasters);
|
||||
} else if (isRibbonList) {
|
||||
let { label, color, position, isTriangleBreak, pointMasters } = parseListElm(line, localColorDict);
|
||||
|
||||
if (label === '"') {
|
||||
label = prevRibbonPointLabel;
|
||||
} else {
|
||||
prevRibbonPointLabel = label;
|
||||
}
|
||||
|
||||
if (color === undefined) {
|
||||
color = ribbonListDefaultColor;
|
||||
}
|
||||
|
||||
ribbonPointLabelArray.push(label);
|
||||
ribbonPointPositionArray.push(...position);
|
||||
ribbonPointBreakArray.push(isTriangleBreak);
|
||||
ribbonPointColorArray.push(color);
|
||||
ribbonPointMasters.push(pointMasters);
|
||||
} else if (isText) {
|
||||
kinemage.texts.push(line);
|
||||
} else if (isCaption) {
|
||||
kinemage.captions.push(line);
|
||||
} else if (line.startsWith('@kinemage')) {
|
||||
kinemage.kinemage = parseInt(line.substr(9).trim());
|
||||
} else if (line.startsWith('@onewidth')) {
|
||||
kinemage.onewidth = true;
|
||||
} else if (line.startsWith('@pdbfile')) {
|
||||
kinemage.pdbfile = parseStr(line);
|
||||
} else if (line.startsWith('@group')) {
|
||||
const { groupName, groupFlags, groupMasters } = parseGroup(line);
|
||||
if (!kinemage.groupDict[groupName as string]) {
|
||||
kinemage.groupDict[groupName as string] = {
|
||||
dominant: false,
|
||||
// If the groupFlags include animate or 2animate, set those to true in the groupDict. Otherwise, set them to false.
|
||||
animate: groupFlags['animate'] ? true : false,
|
||||
'2animate': groupFlags['2animate'] ? true : false,
|
||||
// If the foundAnimate or found2Animate flags are true, set off to true; otherwise set it to the flags value.
|
||||
off: (foundAnimate || found2Animate) ? true : groupFlags['off'] ? true : false
|
||||
};
|
||||
// If the animate or 2animate flags are found in the groupFlags, set foundAnimate
|
||||
// or found2Animate to true, respectively. Also update the list and index.
|
||||
if (groupFlags['animate']) {
|
||||
foundAnimate = true;
|
||||
kinemage.groupsAnimate.push(groupName as string);
|
||||
kinemage.activeAnimateGroup = 0;
|
||||
}
|
||||
if (groupFlags['2animate']) {
|
||||
found2Animate = true;
|
||||
kinemage.groupsAnimate2.push(groupName as string);
|
||||
kinemage.activeAnimateGroup2 = 0;
|
||||
}
|
||||
currentGroupMasters = groupMasters;
|
||||
}
|
||||
currentGroup = groupName;
|
||||
|
||||
if (currentGroupMasters) {
|
||||
currentGroupMasters.forEach(function (master) {
|
||||
if (!kinemage.masterDict[master]) {
|
||||
kinemage.masterDict[master] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in groupFlags as { [k: string]: boolean }) {
|
||||
kinemage.groupDict[groupName as string][key] = (groupFlags as { [k: string]: boolean })[key];
|
||||
}
|
||||
} else if (line.startsWith('@subgroup')) {
|
||||
const { groupName, groupFlags, groupMasters } = parseGroup(line);
|
||||
|
||||
const combinedName = currentGroup + ':' + groupName as string;
|
||||
if (!kinemage.subgroupDict[combinedName]) {
|
||||
kinemage.subgroupDict[combinedName] = {
|
||||
dominant: false,
|
||||
// If the groupFlag includes "off", set off to true; otherwise, set it to false.
|
||||
off: groupFlags['off'] ? true : false,
|
||||
group: currentGroup
|
||||
};
|
||||
currentSubgroupMasters = groupMasters;
|
||||
}
|
||||
currentSubgroup = combinedName;
|
||||
|
||||
if (currentSubgroupMasters) {
|
||||
currentSubgroupMasters.forEach(function (master) {
|
||||
if (!kinemage.masterDict[master]) {
|
||||
kinemage.masterDict[master] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
for (const key in groupFlags as { [k: string]: boolean }) {
|
||||
kinemage.subgroupDict[combinedName as string][key] = (groupFlags as { [k: string]: boolean })[key];
|
||||
}
|
||||
} else if (line.startsWith('@master')) {
|
||||
const name = parseStr(line);
|
||||
const flag = parseFlag(line);
|
||||
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: true
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: There can be more than one flag on a @master line: indent, off, nobutton
|
||||
if (flag === 'on') {
|
||||
kinemage.masterDict[name].visible = true;
|
||||
} else if (flag === 'off') {
|
||||
kinemage.masterDict[name].visible = false;
|
||||
} else if (flag === 'indent') {
|
||||
kinemage.masterDict[name].indent = true;
|
||||
} else if (!flag) {
|
||||
// nothing to do
|
||||
}
|
||||
} else if (line.startsWith('@pointmaster')) {
|
||||
const { name, tags, on } = parsePointmaster(line);
|
||||
if (name.length > 0 && tags.length > 0) {
|
||||
|
||||
// Ensure that we have a masterDict entry for this pointmaster name, even though it doesn't have any flags of its own.
|
||||
if (!kinemage.masterDict[name]) {
|
||||
kinemage.masterDict[name] = {
|
||||
indent: false,
|
||||
visible: on !== false // If the on variable is explicitly false, set visible to false. Otherwise, set it to true.
|
||||
};
|
||||
}
|
||||
|
||||
// Add the mapping to point each single-character tag to the pointmaster name in the pointmasterDict.
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
kinemage.pointmasterDict[tags[i]] = name;
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('@colorset')) {
|
||||
// We have a string inside curly brackets {} followed by the name of an existing dictionary color.
|
||||
const colorName = parseStr(line);
|
||||
const colorReference = parseFlag(line);
|
||||
if (colorReference && colorReference in localColorDict) {
|
||||
localColorDict[colorName] = localColorDict[colorReference];
|
||||
}
|
||||
} else if (/^@(\d*)viewid\b/.test(line)) {
|
||||
const m = line.match(/^@(\d*)viewid\b/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].name = parseStr(line);
|
||||
} else if (/^@(\d*)center\b/.test(line)) {
|
||||
// Match all of the line after center as another string.
|
||||
const m = line.match(/^@(\d*)center\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the three whitespace-separated numbers after the keyword. Parse each as a float and
|
||||
// add them to a length-3 list of numbers.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
// Split on whitespace and take the first three tokens, parsed as floating-point numbers, as the center coordinates.
|
||||
const parts = rest.length > 0 ? rest.split(/\s+/).filter(Boolean) : [];
|
||||
const centerTokens = parts.slice(0, 3).map(parseFloat);
|
||||
// If the length is 3 and all are valid numbers, add the list of three numbers to the view dictionary.
|
||||
if (centerTokens.length === 3 && centerTokens.every(num => !isNaN(num))) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].center = centerTokens;
|
||||
}
|
||||
} else if (/^@(\d*)matrix\b/.test(line)) {
|
||||
// Match all of the line after matrix as another string.
|
||||
const m = line.match(/^@(\d*)matrix\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the nine whitespace-separated numbers after the keyword. Parse each as a float and
|
||||
// add them to a length-9 list of numbers.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
// Split on whitespace and take the first nine tokens, parsed as floating-point numbers, as the matrix values.
|
||||
const parts = rest.length > 0 ? rest.split(/\s+/).filter(Boolean) : [];
|
||||
const matrixTokens = parts.slice(0, 9).map(parseFloat);
|
||||
// If the length is 9 and all are valid numbers, add the list of nine numbers to the view dictionary.
|
||||
if (matrixTokens.length === 9 && matrixTokens.every(num => !isNaN(num))) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].matrix = matrixTokens;
|
||||
}
|
||||
} else if (/^@(\d*)span\b/.test(line)) {
|
||||
// Match all of the line after span as another string.
|
||||
const m = line.match(/^@(\d*)span\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const spanValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(spanValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].span = spanValue;
|
||||
}
|
||||
} else if (/^@(\d*)zoom\b/.test(line)) {
|
||||
// Match all of the line after zoom as another string.
|
||||
const m = line.match(/^@(\d*)zoom\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const zoomValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(zoomValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].zoom = zoomValue;
|
||||
}
|
||||
} else if (/^@(\d*)zslab\b/.test(line)) {
|
||||
// Match all of the line after zslab as another string.
|
||||
const m = line.match(/^@(\d*)zslab\b\s*(.*)$/);
|
||||
const viewCount = (m && m[1] && m[1].length > 0) ? parseInt(m[1], 10) : 1;
|
||||
// Pull out the remainder of the line and parse it as a float.
|
||||
const rest = (m && m[2]) ? m[2].trim() : '';
|
||||
const zslabValue = parseFloat(rest);
|
||||
// If it is a valid number, add it to the view dictionary.
|
||||
if (!isNaN(zslabValue)) {
|
||||
if (!kinemage.viewDict[viewCount]) kinemage.viewDict[viewCount] = {};
|
||||
kinemage.viewDict[viewCount].zslab = zslabValue;
|
||||
}
|
||||
} else {
|
||||
console.log('Kinemage: Unrecognized line: ' + line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Break the file into a list of lines and then parse them all.
|
||||
const lines = data.split(/\r?\n/);
|
||||
_parseChunkOfLines(0, lines.length, lines);
|
||||
|
||||
kinemage.text = kinemage.texts.join('\n').trim();
|
||||
kinemage.caption = kinemage.captions.join('\n').trim();
|
||||
if (kinemage.ribbonLists) {
|
||||
const convertedLists: RibbonObject[] = [];
|
||||
kinemage.ribbonLists.forEach(function (listObject) {
|
||||
convertedLists.push(removePointBreaksTriangleArrays(convertKinTriangleArrays(listObject)));
|
||||
});
|
||||
kinemage.ribbonLists = convertedLists;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export { KinParser };
|
||||
41
src/extensions/kinemage/reader/parser.ts
Normal file
41
src/extensions/kinemage/reader/parser.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { ReaderResult as Result } from '../../../mol-io/reader/result';
|
||||
import { Task, RuntimeContext } from '../../../mol-task';
|
||||
import { Kinemage } from './schema';
|
||||
import { KinParser } from './kinparser';
|
||||
|
||||
async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Kinemage[]>> {
|
||||
const kinemages: Kinemage[] = [];
|
||||
// Split the data into sections based on the '@kinemage' keyword, which indicates one or more kinemages in the file.
|
||||
// Handle the case where there is no '@kinemage' keyword by parsing the entire file.
|
||||
const kinemageSections = data.split(/@kinemage\s+\d+/); // Split based on '@kinemage' keyword followed by a number
|
||||
|
||||
// If there are one or more @kinemage sections, ignore the portion before the first one.
|
||||
// This will either be an empty string (if the first section starts at the beginning of the file)
|
||||
// or header data that is not part of a particular kinemage. This has the effect of removing
|
||||
// the header data even in the case where there is a single @kinemage keyword.
|
||||
if (kinemageSections.length > 1) {
|
||||
kinemageSections.shift();
|
||||
}
|
||||
|
||||
for (const section of kinemageSections) {
|
||||
if (section.trim()) { // Ignore empty sections
|
||||
const NGLParser = new KinParser(section.trim());
|
||||
const kinData = NGLParser.kinemage;
|
||||
kinemages.push(kinData);
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success(kinemages);
|
||||
}
|
||||
|
||||
export function parseKin(data: string) {
|
||||
return Task.create<Result<Kinemage[]>>('Parse KIN', async ctx => {
|
||||
return await parseInternal(data, ctx);
|
||||
});
|
||||
}
|
||||
82
src/extensions/kinemage/reader/schema.ts
Normal file
82
src/extensions/kinemage/reader/schema.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author ReliaSolve <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
import { Color } from '../../../mol-util/color';
|
||||
|
||||
export interface Kinemage {
|
||||
readonly comments: ReadonlyArray<string>
|
||||
kinemage?: number,
|
||||
onewidth?: any,
|
||||
viewDict: { [id: number]: View },
|
||||
pdbfile?: string,
|
||||
text: string,
|
||||
texts: string[],
|
||||
captions: string[],
|
||||
caption: string,
|
||||
groupDict: { [k: string]: { [k: string]: boolean } },
|
||||
subgroupDict: { [k: string]: any }, // /< Subgroup key is "GroupName:SubgroupName" to preserve tree structure
|
||||
masterDict: { [k: string]: { indent: boolean, visible: boolean } },
|
||||
pointmasterDict: { [k: string]: string }, // /< Maps from single-character name to master name for points, e.g. 'a' -> 'alta'
|
||||
dotLists: DotList[],
|
||||
vectorLists: VectorList[],
|
||||
ballLists: BallList[],
|
||||
ribbonLists: RibbonObject[],
|
||||
groupsAnimate: string[],
|
||||
activeAnimateGroup: number,
|
||||
groupsAnimate2: string[],
|
||||
activeAnimateGroup2: number,
|
||||
viewSnapshots?: {} // /< Used to store view snapshots in behavior.ts to use in ui.tsx
|
||||
}
|
||||
|
||||
/** Common base for all list-like objects in a kinemage */
|
||||
export interface KinListBase {
|
||||
name?: string, // /< Optional name of the whole List
|
||||
group: string, // /< Name of the group this List belongs to (may be '' if no group)
|
||||
subgroup: string, // /< Name of the subgroup this List belongs to (may be '' if no subgroup)
|
||||
nobutton: boolean, // /< Whether the list is a nobutton list (true if 'nobutton' keyword found)
|
||||
masterArray: any[], // /< Array of master names per List, not per element
|
||||
pointmasterArray: string[][] // /< Array of point master names per element
|
||||
}
|
||||
|
||||
export interface DotList extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
colorArray: Color[] // /< Color for each element, as many as elements
|
||||
}
|
||||
|
||||
export interface BallList extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
colorArray: Color[], // /< Color for each element, as many as elements
|
||||
radiusArray: number[] // /< A single radius per element
|
||||
}
|
||||
|
||||
export interface RibbonObject extends KinListBase {
|
||||
labelArray: string[], // /< Array of labels per element
|
||||
positionArray: number[], // /< Catenation of x, y, z for each element, 9x as many as triangles (3 vertices per triangle)
|
||||
colorArray: Color[], // /< Color for each element, as many as elements
|
||||
breakArray: boolean[], // /< A single boolean per element indicating if there is a break there
|
||||
pairTriangleNormals: boolean // /< Whether to pair every other triangle normal for lighting (true for ribbons, false for triangles)
|
||||
}
|
||||
|
||||
export interface VectorList extends KinListBase {
|
||||
label1Array: string[], // /< Array of labels for the first half of each element
|
||||
label2Array: string[], // /< Array of labels for the second half of each element
|
||||
position1Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
position2Array: number[], // /< Catenation of x, y, z for each element, 3x as many as elements
|
||||
color1Array: Color[], // /< Color for first half of each element, as many as elements
|
||||
color2Array: Color[], // /< Color for second half of each element, as many as elements
|
||||
width: number[] // /< A single width per element
|
||||
}
|
||||
|
||||
export interface View {
|
||||
name?: string, // /< Optional name of the View
|
||||
center?: number[], // /< X, Y, Z of the center of the view; the model rotates around this point
|
||||
matrix?: number[], // /< Specifies and orthonormal rotation matrix defining view orientation
|
||||
span?: number, // /< Specifies the (smaller of) width or height of the view in world coordinates at the center
|
||||
zoom?: number, // /< Alternate zoom specification, indicates how much of the model is visible, 1=all, 2=half
|
||||
zslab?: number // /< Distance from the center to the near and far clipping planes, 200 means same as span (half is percent of half span)
|
||||
}
|
||||
34
src/extensions/kinemage/representation.ts
Normal file
34
src/extensions/kinemage/representation.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/** Based on the ../anvil extension. */
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../mol-repr/representation';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { StructureRepresentation, StructureRepresentationStateBuilder } from '../../mol-repr/structure/representation';
|
||||
import { ThemeRegistryContext } from '../../mol-theme/theme';
|
||||
|
||||
// TODO: Convert this approach to a more usual one that creates visuals during parse and shows them
|
||||
// during visuals.
|
||||
|
||||
const KinemageDataVisuals = {
|
||||
};
|
||||
|
||||
export const KinemageDataParams = {
|
||||
visuals: PD.MultiSelect([], PD.objectToOptions(KinemageDataVisuals)),
|
||||
};
|
||||
export type KinemageDataParams = typeof KinemageDataParams
|
||||
export type KinemageDataProps = PD.Values<KinemageDataParams>
|
||||
|
||||
export function getKinemageDataParams(ctx: ThemeRegistryContext, structure: Structure) {
|
||||
return PD.clone(KinemageDataParams);
|
||||
}
|
||||
|
||||
export type KinemageDataRepresentation = StructureRepresentation<KinemageDataParams>
|
||||
export function KinemageDataRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, KinemageDataParams>): KinemageDataRepresentation {
|
||||
return Representation.createMulti('Membrane Orientation', ctx, getParams, StructureRepresentationStateBuilder, KinemageDataVisuals as unknown as Representation.Def<Structure, KinemageDataParams>);
|
||||
}
|
||||
305
src/extensions/kinemage/ui.tsx
Normal file
305
src/extensions/kinemage/ui.tsx
Normal file
@@ -0,0 +1,305 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Russ Taylor <russ@reliasolve.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Kinemage right-panel controls (right-panel only).
|
||||
*
|
||||
* Shows kinemage views, animate buttons, and group/subgroup/master toggles in the right inspector.
|
||||
* Controls directly operate on the loaded kinemage runtime data and call exported helpers
|
||||
* to rebuild visuals. No State Tree JSON nodes are created for these UI items.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { CollapsableState, CollapsableControls } from '../../mol-plugin-ui/base';
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { applyViewSnapshot, rebuildShapesForKinemage } from './behavior';
|
||||
import { Kinemage } from './reader/schema';
|
||||
|
||||
interface KinemageControlState extends CollapsableState {
|
||||
isBusy: boolean
|
||||
}
|
||||
|
||||
function nameFromString(s: string | undefined) {
|
||||
// If this is undefined, return undefined.
|
||||
if (!s) return undefined;
|
||||
// Return up to the first 30 characters of the string.
|
||||
return s.length > 30 ? s.substring(0, 30) + '...' : s;
|
||||
}
|
||||
|
||||
export class KinemageControls extends CollapsableControls<{}, KinemageControlState> {
|
||||
protected defaultState(): KinemageControlState {
|
||||
return {
|
||||
header: 'Kinemage',
|
||||
isCollapsed: false,
|
||||
isBusy: false,
|
||||
// default hidden until a kinemage is present
|
||||
isHidden: true,
|
||||
brand: { accent: 'cyan', svg: undefined as any }
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Listen for shape/state changes: when state tree cells are created or removed the visuals changed.
|
||||
this.subscribe(this.plugin.state.data.events.cell.created, (e: any) => this.onCellCreated(e));
|
||||
this.subscribe(this.plugin.state.data.events.cell.removed, () => this.onCellRemoved());
|
||||
// also track cell state updates that may change labels / visibility
|
||||
this.subscribe(this.plugin.state.data.events.cell.stateUpdated, () => this.forceUpdate());
|
||||
|
||||
// ensure initial visibility reflects current state
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellCreated(e: any) {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private onCellRemoved() {
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
private updateVisibility() {
|
||||
const kinemages = this.getKinemageList();
|
||||
this.setState({ isHidden: kinemages.length === 0 });
|
||||
}
|
||||
|
||||
private getKinemageList(): Array<{ kinData: Kinemage, ref: string }> {
|
||||
const result: Array<{ kinData: Kinemage, ref: string }> = [];
|
||||
|
||||
try {
|
||||
const cells = (this.plugin.state.data as any).cells as Map<string, any>;
|
||||
for (const [ref, entry] of cells) {
|
||||
const obj = (entry as any).obj;
|
||||
// Look for Format.Json nodes that contain kinData
|
||||
if (obj && obj.data && (obj.data as any).kinData) {
|
||||
result.push({ kinData: (obj.data as any).kinData, ref });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to enumerate kinemage nodes', e);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async applyView(kinData: Kinemage, viewKey: string) {
|
||||
const snap = (kinData as any).viewSnapshots?.[viewKey];
|
||||
if (snap) {
|
||||
await applyViewSnapshot(this.plugin, snap as Partial<Camera.Snapshot>);
|
||||
}
|
||||
}
|
||||
|
||||
private async toggleVisibility(kinData: Kinemage, kinRef: string, target: { type: 'group' | 'subgroup' | 'master', key: string }) {
|
||||
try {
|
||||
if (target.type === 'group') {
|
||||
const g = kinData.groupDict[target.key];
|
||||
if (g) g.off = !g.off;
|
||||
} else if (target.type === 'subgroup') {
|
||||
const s = kinData.subgroupDict[target.key];
|
||||
if (s) s.off = !s.off;
|
||||
} else {
|
||||
const m = kinData.masterDict[target.key];
|
||||
if (m) m.visible = !m.visible;
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to toggle kinemage visibility', e);
|
||||
}
|
||||
}
|
||||
|
||||
private async triggerAnimateForKin(kinData: Kinemage, kinRef: string, mode: 'animate' | '2animate') {
|
||||
try {
|
||||
if (mode === 'animate') {
|
||||
kinData.activeAnimateGroup = (kinData.activeAnimateGroup + 1) % Math.max(1, kinData.groupsAnimate.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate.length; i++) {
|
||||
const groupName = kinData.groupsAnimate[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
kinData.activeAnimateGroup2 = (kinData.activeAnimateGroup2 + 1) % Math.max(1, kinData.groupsAnimate2.length);
|
||||
|
||||
// Make only the active animate group visible, hide the others (if any)
|
||||
for (let i = 0; i < kinData.groupsAnimate2.length; i++) {
|
||||
const groupName = kinData.groupsAnimate2[i];
|
||||
const groupInfo = kinData.groupDict[groupName];
|
||||
if (groupInfo) {
|
||||
groupInfo.off = (i !== kinData.activeAnimateGroup2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild shapes for this kinemage using the state ref
|
||||
await rebuildShapesForKinemage(this.plugin, { ref: kinRef } as any);
|
||||
this.updateVisibility();
|
||||
} catch (e) {
|
||||
console.error('Failed to trigger animate', e);
|
||||
}
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
const kins = this.getKinemageList();
|
||||
if (kins.length === 0) return <div style={{ padding: '6px' }}>No Kinemage data</div>;
|
||||
|
||||
const blocks: React.ReactNode[] = [];
|
||||
for (const { kinData, ref } of kins) {
|
||||
const title = kinData.pdbfile || nameFromString(kinData.caption) || 'Kinemage';
|
||||
const kinBlock: React.ReactNode[] = [];
|
||||
|
||||
// Title
|
||||
kinBlock.push(
|
||||
<div key={'title-' + title} style={{ padding: '6px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
|
||||
// views
|
||||
const viewEntries = Object.entries(kinData.viewDict || {});
|
||||
if (viewEntries.length > 0) {
|
||||
for (const [viewKey, viewObj] of viewEntries) {
|
||||
const label = `View ${viewObj.name || `View ${viewKey}`}`;
|
||||
kinBlock.push(
|
||||
<div key={'view-' + title + '-' + viewKey} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.applyView(kinData, viewKey)}
|
||||
title={`Apply view: ${label}`}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// animate
|
||||
if (kinData.groupsAnimate && kinData.groupsAnimate.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, 'animate')}
|
||||
title='Cycle through animation frames'
|
||||
>
|
||||
Animate
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (kinData.groupsAnimate2 && kinData.groupsAnimate2.length > 0) {
|
||||
kinBlock.push(
|
||||
<div key={'anim2-' + title} style={{ padding: '2px 6px' }}>
|
||||
<button
|
||||
className='msp-btn msp-btn-block'
|
||||
onClick={() => this.triggerAnimateForKin(kinData, ref, '2animate')}
|
||||
title='Cycle through second animation frames'
|
||||
>
|
||||
Animate2
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// groups
|
||||
for (const [groupKey, groupInfo] of Object.entries(kinData.groupDict || {})) {
|
||||
if (!(groupInfo as any).nobutton) {
|
||||
const visible = !(groupInfo as any).off;
|
||||
// If this group is in animate or animate2, then add '*' before its groupKey name to indicate that it's an animation group
|
||||
const isAnimate = kinData.groupsAnimate?.includes(groupKey) || kinData.groupsAnimate2?.includes(groupKey);
|
||||
const label = isAnimate ? `* ${groupKey}` : groupKey;
|
||||
kinBlock.push(
|
||||
<div key={'group-' + title + '-' + groupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'group', key: groupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={label}>{label}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// If this group is not dominant, find any subgroups of this group and show them here (indented) unless they have nobutton set
|
||||
if (!(groupInfo as any).dominant) {
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
if (subgroupKey.startsWith(groupKey + ':')) {
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
const subgroupLabel = subgroupKey.split(':')[1];
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px', paddingLeft: '24px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupLabel}>{subgroupLabel}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// subgroups that don't belong to a group (standalone)
|
||||
for (const [subgroupKey, subgroupInfo] of Object.entries(kinData.subgroupDict || {})) {
|
||||
// if parent group present, those groups' subgroups are already shown when iterating groups
|
||||
if (subgroupKey.indexOf(':') !== -1) {
|
||||
// subgroups with parent group; skip here (shown under parent group)
|
||||
continue;
|
||||
}
|
||||
if ((subgroupInfo as any).nobutton) continue;
|
||||
const visible = !(subgroupInfo as any).off;
|
||||
kinBlock.push(
|
||||
<div key={'subgroup-' + title + '-' + subgroupKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'subgroup', key: subgroupKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={subgroupKey}>{subgroupKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// masters
|
||||
for (const [masterKey, masterInfo] of Object.entries(kinData.masterDict || {})) {
|
||||
const visible = !!(masterInfo && (masterInfo as any).visible);
|
||||
kinBlock.push(
|
||||
<div key={'master-' + title + '-' + masterKey} style={{ padding: '2px 6px' }}>
|
||||
<label style={{ display: 'flex', alignItems: 'center', cursor: 'pointer' }}>
|
||||
<input
|
||||
type='checkbox'
|
||||
checked={visible}
|
||||
onChange={() => this.toggleVisibility(kinData, ref, { type: 'master', key: masterKey })}
|
||||
style={{ marginRight: '6px' }}
|
||||
/>
|
||||
<span title={masterKey}>{masterKey}</span>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
blocks.push(<div key={'kin-block-' + title} className='msp-control-group-wrapper'>{kinBlock}</div>);
|
||||
}
|
||||
|
||||
return <>{blocks}</>;
|
||||
}
|
||||
}
|
||||
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
119
src/extensions/mvs/load-extensions/volume-streaming.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { VolumeStreaming } from '../../../mol-plugin/behavior/dynamic/volume-streaming/behavior';
|
||||
import { CreateVolumeStreamingBehavior, CreateVolumeStreamingInfo, VolumeStreamingVisual } from '../../../mol-plugin/behavior/dynamic/volume-streaming/transformers';
|
||||
import { mapObjectMap } from '../../../mol-util/object';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
import { MolstarLoadingExtension } from '../load';
|
||||
import { UpdateTarget } from '../load-generic';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
|
||||
/** Type of `molstar_volume_streaming` custom property, used by `VolumeStreamingExtension` MVS loading extension. */
|
||||
export type MolstarVolumeStreamingCustomProp = {
|
||||
/** URL of the volume streaming server, e.g. 'https://www.ebi.ac.uk/pdbe/densities'. */
|
||||
server_url?: string,
|
||||
/** Volume streaming view type ('off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'). Default value depends on structure type (X-ray/EM). */
|
||||
view?: VolumeStreaming.ViewTypes,
|
||||
/** Customization of channel parameters. */
|
||||
channel_params?: { [name in VolumeStreaming.ChannelType]?: Partial<ChannelParams_> },
|
||||
/** List of volume streaming entries (if not specified, will be retrieved automatically based on PDB ID) */
|
||||
entries?: ReturnType<typeof CreateVolumeStreamingInfo['createDefaultParams']>['entries'],
|
||||
} | boolean | undefined;
|
||||
|
||||
|
||||
/** This MVS loading extension allows turning on volume streaming for a structure by providing custom property `molstar_volume_streaming`.
|
||||
*
|
||||
* Examples:
|
||||
*
|
||||
* ```
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1cbs_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: true,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1tqn_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* channel_params: {
|
||||
* '2fo-fc': { color: 'skyblue', opacity: 0.3 },
|
||||
* 'fo-fc(+ve)': { color: 'greenyellow', wireframe: true, isoValue: { kind: 'relative', relativeValue: +2.5 } },
|
||||
* 'fo-fc(-ve)': { color: 'orange', wireframe: true, isoValue: { kind: 'relative', relativeValue: -2.5 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
*
|
||||
* builder
|
||||
* .download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/8hra_updated.cif' })
|
||||
* .parse({ format: 'mmcif' })
|
||||
* .modelStructure({
|
||||
* custom: {
|
||||
* molstar_volume_streaming: {
|
||||
* server_url: 'https://www.ebi.ac.uk/pdbe/densities', // = default
|
||||
* entries: [{ dataId: 'EMD-34965', source: { name: 'em', params: { isoValue: { kind: 'absolute', absoluteValue: 0.015 } } } }],
|
||||
* view: 'auto', // default is 'auto' for EM, 'selection-box' for X-ray structures
|
||||
* channel_params: {
|
||||
* em: { color: '#ff0000', opacity: 0.4, isoValue: { kind: 'absolute', absoluteValue: 0.025 } },
|
||||
* },
|
||||
* } satisfies MolstarVolumeStreamingCustomProp,
|
||||
* },
|
||||
* })
|
||||
* .component()
|
||||
* .representation();
|
||||
* ```
|
||||
*/
|
||||
export const VolumeStreamingExtension: MolstarLoadingExtension<{}> = {
|
||||
id: 'wwpdb/volume-streaming',
|
||||
description: 'Allow turning on volume streaming for a structure',
|
||||
createExtensionContext: () => ({}),
|
||||
action: (updateTarget, node, context, extContext) => {
|
||||
if (node.kind !== 'structure') return;
|
||||
let params: MolstarVolumeStreamingCustomProp = node.custom?.molstar_volume_streaming;
|
||||
if (!params) return;
|
||||
if (params === true) params = {};
|
||||
|
||||
const streamingInfo = UpdateTarget.apply(updateTarget, CreateVolumeStreamingInfo, {
|
||||
serverUrl: params.server_url,
|
||||
autoEntries: !params.entries,
|
||||
entries: params.entries,
|
||||
defaultView: params.view,
|
||||
defaultChannelParams: params.channel_params && mapObjectMap(params.channel_params, normalizeChannelParams),
|
||||
}, { state: { isCollapsed: true } });
|
||||
|
||||
const streamingBehavior = UpdateTarget.apply(streamingInfo, CreateVolumeStreamingBehavior);
|
||||
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: '2fo-fc' }, { state: { isGhost: true }, tags: '2fo-fc' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(+ve)' }, { state: { isGhost: true }, tags: 'fo-fc(+ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'fo-fc(-ve)' }, { state: { isGhost: true }, tags: 'fo-fc(-ve)' });
|
||||
UpdateTarget.apply(streamingBehavior, VolumeStreamingVisual, { channel: 'em' }, { state: { isGhost: true }, tags: 'em' });
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
interface ChannelParams_ extends Omit<VolumeStreaming.ChannelParams, 'color'> {
|
||||
color: ColorT | number,
|
||||
}
|
||||
|
||||
function normalizeChannelParams(p: Partial<ChannelParams_> | undefined): Partial<VolumeStreaming.ChannelParams> | undefined {
|
||||
if (!p) return undefined;
|
||||
return {
|
||||
...p,
|
||||
color: decodeColor(p.color),
|
||||
};
|
||||
}
|
||||
@@ -405,6 +405,15 @@ function representationPropsBase(node: MolstarSubtree<'representation'>): Partia
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
case 'putty': {
|
||||
const sizeTheme = params.size_theme ?? 'uniform';
|
||||
return {
|
||||
type: { name: 'putty', params: { alpha, sizeFactor: params.size_factor } },
|
||||
sizeTheme: sizeTheme === 'uncertainty'
|
||||
? { name: 'uncertainty', params: {} }
|
||||
: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import { MVSTrajectoryWithCoordinates } from './components/trajectory';
|
||||
import { generateStateTransition } from './helpers/animation';
|
||||
import { IsHiddenCustomStateExtension } from './load-extensions/is-hidden-custom-state';
|
||||
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
|
||||
import { VolumeStreamingExtension } from './load-extensions/volume-streaming';
|
||||
import { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, clippingForNode, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformAndInstantiateStructure, transformAndInstantiateVolume, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, Snapshot, SnapshotMetadata } from './mvs-data';
|
||||
@@ -194,6 +195,7 @@ function molstarTreeToEntry(
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, { previousTransitionDurationMs: metadata.previousTransitionDurationMs });
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
snapshot.structureFocus = {}; // avoid structure focus persisting through states (causes weird behaviors, e.g. when turning on Volume Streaming)
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
@@ -502,4 +504,5 @@ export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<Molsta
|
||||
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
|
||||
NonCovalentInteractionsExtension,
|
||||
IsHiddenCustomStateExtension,
|
||||
VolumeStreamingExtension,
|
||||
];
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Zachary Charlop-Powers <zach.charlop.powers@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData } from '../../../mvs-data';
|
||||
@@ -17,6 +18,42 @@ describe('mvs-builder', () => {
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty' });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works with uniform size_theme', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty', size_theme: 'uniform', size_factor: 0.5 });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('putty representation works with uncertainty size_theme', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
.download({ url: 'https://files.rcsb.org/download/1cbs.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure()
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'putty', size_theme: 'uncertainty' });
|
||||
const state = builder.getState();
|
||||
expect(MVSData.validationIssues(state)).toEqual(undefined);
|
||||
});
|
||||
|
||||
it('volume builder works', () => {
|
||||
const builder = createMVSBuilder();
|
||||
builder
|
||||
|
||||
@@ -46,6 +46,13 @@ const Carbohydrate = {
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
};
|
||||
|
||||
const Putty = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls how the tube radius is determined. */
|
||||
size_theme: OptionalField(literal('uniform', 'uncertainty'), 'uniform', "Controls how the tube radius is determined. 'uniform' uses a constant radius scaled by size_factor. 'uncertainty' drives the radius from per-residue B-factor/RMSF values."),
|
||||
};
|
||||
|
||||
const Surface = {
|
||||
/** Type of surface representation. (Default is 'molecular') */
|
||||
surface_type: OptionalField(literal('molecular', 'gaussian'), 'molecular', `Type of surface representation. (Default is 'molecular')`),
|
||||
@@ -66,6 +73,7 @@ export const MVSRepresentationParams = UnionParamsSchema(
|
||||
spacefill: SimpleParamsSchema(Spacefill),
|
||||
carbohydrate: SimpleParamsSchema(Carbohydrate),
|
||||
surface: SimpleParamsSchema(Surface),
|
||||
putty: SimpleParamsSchema(Putty),
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { Viewport, cameraProject, cameraUnproject } from './camera/util';
|
||||
import { CameraTransitionManager } from './camera/transition';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { BehaviorSubject, Subject } from 'rxjs';
|
||||
import { Scene } from '../mol-gl/scene';
|
||||
import { assertUnreachable } from '../mol-util/type-helpers';
|
||||
import { Ray3D } from '../mol-math/geometry/primitives/ray3d';
|
||||
@@ -15,6 +15,7 @@ import { Mat4 } from '../mol-math/linear-algebra/3d/mat4';
|
||||
import { Vec4 } from '../mol-math/linear-algebra/3d/vec4';
|
||||
import { Vec3 } from '../mol-math/linear-algebra/3d/vec3';
|
||||
import { EPSILON } from '../mol-math/linear-algebra/3d/common';
|
||||
import { Euler } from '../mol-math/linear-algebra/3d/euler';
|
||||
|
||||
export type { ICamera };
|
||||
|
||||
@@ -42,6 +43,12 @@ interface ICamera {
|
||||
}
|
||||
|
||||
const tmpClip = Vec4();
|
||||
const tmpForward = Vec3();
|
||||
const tmpRight = Vec3();
|
||||
const tmpUp = Vec3();
|
||||
const tmpBack = Vec3();
|
||||
const tmpDelta = Vec3();
|
||||
const tmpRotMat = Mat4.identity();
|
||||
|
||||
export class Camera implements ICamera {
|
||||
readonly view: Mat4 = Mat4.identity();
|
||||
@@ -70,6 +77,8 @@ export class Camera implements ICamera {
|
||||
|
||||
readonly transition: CameraTransitionManager = new CameraTransitionManager(this);
|
||||
readonly stateChanged = new BehaviorSubject<Partial<Camera.Snapshot>>(this.state);
|
||||
/** Fires whenever update() produces a changed view/projection (covers all mutations, including direct ones from controls). */
|
||||
readonly changed = new Subject<void>();
|
||||
|
||||
get position() { return this.state.position; }
|
||||
set position(v: Vec3) { Vec3.copy(this.state.position, v); }
|
||||
@@ -123,6 +132,7 @@ export class Camera implements ICamera {
|
||||
|
||||
Mat4.copy(this.prevView, this.view);
|
||||
Mat4.copy(this.prevProjection, this.projection);
|
||||
this.changed.next();
|
||||
}
|
||||
|
||||
return changed;
|
||||
@@ -237,6 +247,57 @@ export class Camera implements ICamera {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** How much the camera is rotated around its target. Uses 'ZYX' order. */
|
||||
getRotation(out: Euler) {
|
||||
const { position, target, up } = this.state;
|
||||
Vec3.normalize(tmpForward, Vec3.sub(tmpForward, target, position));
|
||||
Vec3.normalize(tmpRight, Vec3.cross(tmpRight, tmpForward, up));
|
||||
Vec3.cross(tmpUp, tmpRight, tmpForward);
|
||||
|
||||
Mat4.setIdentity(tmpRotMat);
|
||||
tmpRotMat[0] = tmpRight[0]; tmpRotMat[1] = tmpRight[1]; tmpRotMat[2] = tmpRight[2];
|
||||
tmpRotMat[4] = tmpUp[0]; tmpRotMat[5] = tmpUp[1]; tmpRotMat[6] = tmpUp[2];
|
||||
tmpRotMat[8] = -tmpForward[0]; tmpRotMat[9] = -tmpForward[1]; tmpRotMat[10] = -tmpForward[2];
|
||||
|
||||
return Euler.fromMat4(out, tmpRotMat, 'ZYX');
|
||||
}
|
||||
|
||||
/** Set the camera rotation around its target. Expects 'ZYX' order. */
|
||||
setRotation(rotation: Euler, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
const distance = Vec3.distance(snapshot.position, snapshot.target);
|
||||
|
||||
Mat4.fromEuler(tmpRotMat, rotation, 'ZYX');
|
||||
|
||||
// back = R * (0,0,1) → column 2 of R
|
||||
Vec3.set(tmpBack, tmpRotMat[8], tmpRotMat[9], tmpRotMat[10]);
|
||||
// up = R * (0,1,0) → column 1 of R
|
||||
Vec3.set(tmpUp, tmpRotMat[4], tmpRotMat[5], tmpRotMat[6]);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.scaleAndAdd(state.position, snapshot.target, tmpBack, distance);
|
||||
Vec3.copy(state.up, tmpUp);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
/** Translation of the camera target relative to world origin (0, 0, 0) */
|
||||
getTranslation(out: Vec3) {
|
||||
return Vec3.copy(out, this.state.target);
|
||||
}
|
||||
|
||||
/** Set the camera target to the given translation, moving position by the same delta so orientation/distance are preserved */
|
||||
setTranslation(translation: Vec3, durationMs?: number) {
|
||||
const snapshot = this.state as Camera.Snapshot;
|
||||
Vec3.sub(tmpDelta, translation, snapshot.target);
|
||||
|
||||
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), snapshot);
|
||||
Vec3.add(state.position, snapshot.position, tmpDelta);
|
||||
Vec3.copy(state.target, translation);
|
||||
|
||||
this.setState(state, durationMs);
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
|
||||
this.viewport = viewport;
|
||||
Camera.copySnapshot(this.state, state);
|
||||
|
||||
@@ -23,7 +23,7 @@ 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 { DebugHelperParams } from './helper/bounding-sphere-helper';
|
||||
import { DebugRegistry } from './helper/debug-registry';
|
||||
import { SetUtils } from '../mol-util/set';
|
||||
import { Canvas3dInteractionHelper, Canvas3dInteractionHelperParams } from './helper/interaction-events';
|
||||
import { PostprocessingParams } from './passes/postprocessing';
|
||||
@@ -98,6 +98,7 @@ export const Canvas3DParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
checkeredTransparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable GPU time-based animations (wiggle/tumble).' }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
|
||||
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
|
||||
|
||||
@@ -109,7 +110,6 @@ export const Canvas3DParams = {
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
interaction: PD.Group(Canvas3dInteractionHelperParams),
|
||||
debug: PD.Group(DebugHelperParams),
|
||||
handle: PD.Group(HandleHelperParams),
|
||||
pointer: PD.Group(PointerHelperParams),
|
||||
xr: PD.Group(XRManagerParams, { label: 'XR' }),
|
||||
@@ -388,6 +388,8 @@ interface Canvas3D {
|
||||
readonly stats: RendererStats
|
||||
readonly interaction: Canvas3dInteractionHelper['events']
|
||||
|
||||
readonly debugRegistry: DebugRegistry
|
||||
|
||||
readonly xr: {
|
||||
request(): Promise<void>
|
||||
end(): Promise<void>
|
||||
@@ -478,6 +480,7 @@ namespace Canvas3D {
|
||||
const hiZ = new HiZPass(webgl, passes.draw, canvas, p.hiZ);
|
||||
|
||||
const renderer = Renderer.create(webgl, p.renderer);
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
const shaderManager = new ShaderManager(webgl, scene);
|
||||
@@ -674,7 +677,8 @@ namespace Canvas3D {
|
||||
const xrChanged = xrManager.update(xrFrame);
|
||||
if (!xrChanged && xrFrame) return false;
|
||||
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged;
|
||||
const activeAnimation = p.enableAnimation && scene.hasAnimation;
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender || xrChanged || activeAnimation;
|
||||
forceNextRender = false;
|
||||
|
||||
if (passes.illumination.supported && p.illumination.enabled && !xrFrame) {
|
||||
@@ -753,6 +757,7 @@ namespace Canvas3D {
|
||||
if (webgl.xr.session && !options?.xrFrame) return;
|
||||
|
||||
currentTime = t;
|
||||
renderer.setTime((currentTime - startTime) / 1000);
|
||||
commit(options?.isSynchronous);
|
||||
|
||||
// update the controler before the camera transition
|
||||
@@ -1066,6 +1071,7 @@ namespace Canvas3D {
|
||||
transparentBackground: p.transparentBackground,
|
||||
checkeredTransparentBackground: p.checkeredTransparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
enableAnimation: p.enableAnimation,
|
||||
pickPadding: p.pickPadding,
|
||||
userInteractionReleaseMs: p.userInteractionReleaseMs,
|
||||
viewport: p.viewport,
|
||||
@@ -1078,7 +1084,6 @@ namespace Canvas3D {
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
interaction: { ...interactionHelper.props },
|
||||
debug: { ...helper.debug.props },
|
||||
handle: { ...helper.handle.props },
|
||||
pointer: { ...helper.pointer.props },
|
||||
xr: { ...xrManager.props },
|
||||
@@ -1312,6 +1317,10 @@ namespace Canvas3D {
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.checkeredTransparentBackground !== undefined) p.checkeredTransparentBackground = props.checkeredTransparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.enableAnimation !== undefined) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
renderer.setProps({ enableAnimation: p.enableAnimation });
|
||||
}
|
||||
if (props.pickPadding !== undefined) {
|
||||
p.pickPadding = props.pickPadding;
|
||||
pickHelper.setPickPadding(p.pickPadding);
|
||||
@@ -1348,7 +1357,6 @@ namespace Canvas3D {
|
||||
}
|
||||
if (props.trackball) controls.setProps(props.trackball);
|
||||
if (props.interaction) interactionHelper.setProps(props.interaction);
|
||||
if (props.debug) helper.debug.setProps(props.debug);
|
||||
if (props.handle) helper.handle.setProps(props.handle);
|
||||
if (props.pointer) helper.pointer.setProps(props.pointer);
|
||||
if (props.xr) xrManager.setProps(props.xr);
|
||||
@@ -1399,6 +1407,7 @@ namespace Canvas3D {
|
||||
get interaction() {
|
||||
return interactionHelper.events;
|
||||
},
|
||||
debugRegistry: helper.debug,
|
||||
xr,
|
||||
dispose: () => {
|
||||
contextLostSub?.unsubscribe();
|
||||
|
||||
85
src/mol-canvas3d/helper/debug-registry.ts
Normal file
85
src/mol-canvas3d/helper/debug-registry.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2026 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 { isDebugMode } from '../../mol-util/debug';
|
||||
|
||||
export interface DebugHelper<T extends {} = {}> {
|
||||
readonly scene: Scene;
|
||||
update(): void;
|
||||
syncVisibility(): void;
|
||||
clear(): void;
|
||||
readonly isEnabled: boolean;
|
||||
readonly props: T;
|
||||
setProps(props: Partial<T>): void;
|
||||
}
|
||||
|
||||
export class DebugRegistry {
|
||||
readonly ctx: WebGLContext;
|
||||
readonly parent: Scene;
|
||||
|
||||
private readonly entries = new Map<string, DebugHelper>();
|
||||
|
||||
constructor(ctx: WebGLContext, parent: Scene) {
|
||||
this.ctx = ctx;
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
register<T extends {}>(name: string, entry: DebugHelper<T>) {
|
||||
if (this.entries.has(name)) {
|
||||
if (isDebugMode) {
|
||||
console.warn(`Debug helper with name '${name}' already exists, replacing.`);
|
||||
}
|
||||
this.entries.get(name)!.clear();
|
||||
}
|
||||
this.entries.set(name, entry);
|
||||
}
|
||||
|
||||
unregister(name: string) {
|
||||
const entry = this.entries.get(name);
|
||||
if (entry) {
|
||||
entry.clear();
|
||||
this.entries.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
get scenes(): Scene[] {
|
||||
return Array.from(this.entries.values()).map(e => e.scene);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.entries.forEach(entry => {
|
||||
if (entry.isEnabled) entry.update();
|
||||
});
|
||||
}
|
||||
|
||||
syncVisibility() {
|
||||
this.entries.forEach(entry => {
|
||||
entry.syncVisibility();
|
||||
});
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.entries.forEach(entry => {
|
||||
entry.clear();
|
||||
});
|
||||
}
|
||||
|
||||
get isEnabled() {
|
||||
let enabled = false;
|
||||
this.entries.forEach(entry => {
|
||||
if (entry.isEnabled) enabled = true;
|
||||
});
|
||||
return enabled;
|
||||
}
|
||||
|
||||
setProps<T extends {}>(props: Partial<T>) {
|
||||
this.entries.forEach(entry => {
|
||||
entry.setProps(props);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,13 +7,12 @@
|
||||
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 { DebugRegistry } from './debug-registry';
|
||||
import { CameraHelper, CameraHelperParams } from './camera-helper';
|
||||
import { HandleHelper, HandleHelperParams } from './handle-helper';
|
||||
import { PointerHelper, PointerHelperParams } from './pointer-helper';
|
||||
|
||||
export const HelperParams = {
|
||||
debug: PD.Group(DebugHelperParams),
|
||||
camera: PD.Group({
|
||||
helper: PD.Group(CameraHelperParams)
|
||||
}),
|
||||
@@ -25,7 +24,7 @@ export type HelperProps = PD.Values<typeof HelperParams>
|
||||
|
||||
|
||||
export class Helper {
|
||||
readonly debug: BoundingSphereHelper;
|
||||
readonly debug: DebugRegistry;
|
||||
readonly camera: CameraHelper;
|
||||
readonly handle: HandleHelper;
|
||||
readonly pointer: PointerHelper;
|
||||
@@ -33,7 +32,7 @@ export class Helper {
|
||||
constructor(webgl: WebGLContext, scene: Scene, props: Partial<HelperProps> = {}) {
|
||||
const p = { ...DefaultHelperProps, ...props };
|
||||
|
||||
this.debug = new BoundingSphereHelper(webgl, scene, p.debug);
|
||||
this.debug = new DebugRegistry(webgl, scene);
|
||||
this.camera = new CameraHelper(webgl, p.camera.helper);
|
||||
this.handle = new HandleHelper(webgl, p.handle);
|
||||
this.pointer = new PointerHelper(webgl, p.pointer);
|
||||
|
||||
@@ -121,7 +121,7 @@ export class PointerHelper {
|
||||
|
||||
this.camera = new Camera();
|
||||
|
||||
this.shape = getPointerMeshShape(this.getData(), this.props, this.shape);
|
||||
this.shape = getPointerMeshShape(this.getData(), this.props);
|
||||
this.renderObject = createMeshRenderObject(this.shape, this.props);
|
||||
this.scene.add(this.renderObject);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
@@ -446,18 +446,28 @@ export class DrawPass {
|
||||
target.bind();
|
||||
}
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera);
|
||||
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.attachFramebuffer(target.framebuffer, 'depth');
|
||||
}
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
for (const scene of helper.debug.scenes) {
|
||||
renderer.renderBlended(scene, camera);
|
||||
}
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.detachFramebuffer(target.framebuffer, 'depth');
|
||||
}
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2024-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2024-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -329,6 +329,31 @@ export class IlluminationPass {
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
this.renderInput(renderer, camera, scene, props);
|
||||
|
||||
this.transparentTarget.bind();
|
||||
if (helper.debug.isEnabled || helper.pointer.isEnabled) {
|
||||
this.drawPass.depthTextureOpaque.attachFramebuffer(this.transparentTarget.framebuffer, 'depth');
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
for (const scene of helper.debug.scenes) {
|
||||
renderer.renderBlended(scene, camera);
|
||||
}
|
||||
}
|
||||
if (helper.pointer.isEnabled) {
|
||||
helper.pointer.setCamera(camera);
|
||||
renderer.update(helper.pointer.camera, helper.pointer.scene);
|
||||
renderer.renderBlended(helper.pointer.scene, helper.pointer.camera);
|
||||
}
|
||||
this.drawPass.depthTextureOpaque.detachFramebuffer(this.transparentTarget.framebuffer, 'depth');
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
}
|
||||
|
||||
state.disable(gl.BLEND);
|
||||
@@ -433,19 +458,6 @@ export class IlluminationPass {
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera);
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
let targetIsDrawingbuffer = false;
|
||||
|
||||
@@ -577,3 +577,25 @@ export class PCG {
|
||||
return this.int() / 0x100000000;
|
||||
}
|
||||
}
|
||||
|
||||
export function mortonOrder3d(x: number, y: number, z: number): number {
|
||||
let out = 0;
|
||||
for (let i = 0; i < 21; ++i) {
|
||||
out |= ((x >> i) & 1) << (3 * i + 2);
|
||||
out |= ((y >> i) & 1) << (3 * i + 1);
|
||||
out |= ((z >> i) & 1) << (3 * i);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function invertMortonOrder3d(code: number): [number, number, number] {
|
||||
let x = 0;
|
||||
let y = 0;
|
||||
let z = 0;
|
||||
for (let i = 0; i < 21; ++i) {
|
||||
x |= ((code >> (3 * i + 2)) & 1) << i;
|
||||
y |= ((code >> (3 * i + 1)) & 1) << i;
|
||||
z |= ((code >> (3 * i)) & 1) << i;
|
||||
}
|
||||
return [x, y, z];
|
||||
}
|
||||
|
||||
64
src/mol-geo/geometry/animation.ts
Normal file
64
src/mol-geo/geometry/animation.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
export type AnimationData = {
|
||||
uWiggleSpeed: ValueCell<number>,
|
||||
uWiggleAmplitude: ValueCell<number>,
|
||||
uWiggleFrequency: ValueCell<number>,
|
||||
uWiggleMode: ValueCell<number>,
|
||||
uTumbleSpeed: ValueCell<number>,
|
||||
uTumbleAmplitude: ValueCell<number>,
|
||||
uTumbleFrequency: ValueCell<number>,
|
||||
}
|
||||
|
||||
export function getAnimationParam() {
|
||||
return PD.Group({
|
||||
wiggleMode: PD.Select('position', [['position', 'Position'], ['group', 'Group']] as const, { description: 'Noise seeding mode. Position: spatially correlated (nearby atoms move together). Group: per-group independent noise.' }),
|
||||
wiggleSpeed: PD.Numeric(7, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of vertex wiggle animation.' }),
|
||||
wiggleAmplitude: PD.Numeric(0, { min: 0, max: 5, step: 0.01 }, { description: 'Amplitude of vertex wiggle animation.' }),
|
||||
wiggleFrequency: PD.Numeric(0.2, { min: 0.01, max: 2, step: 0.01 }, { description: 'Spatial frequency of vertex wiggle noise (position mode). Lower values correlate nearby atoms more.' }),
|
||||
tumbleSpeed: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }, { description: 'Speed of instance tumble animation.' }),
|
||||
tumbleAmplitude: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, { description: 'Amplitude of instance tumble animation. In Ångströms of implied surface displacement.' }),
|
||||
tumbleFrequency: PD.Numeric(0.2, { min: 0, max: 2, step: 0.01 }, { description: 'Spatial frequency multiplier for tumble noise.' }),
|
||||
});
|
||||
}
|
||||
export type AnimationParam = ReturnType<typeof getAnimationParam>
|
||||
export type AnimationProps = AnimationParam['defaultValue'];
|
||||
|
||||
export function areAnimationPropsEqual(a: AnimationProps, b: AnimationProps): boolean {
|
||||
return a.wiggleMode === b.wiggleMode
|
||||
&& a.wiggleSpeed === b.wiggleSpeed
|
||||
&& a.wiggleAmplitude === b.wiggleAmplitude
|
||||
&& a.wiggleFrequency === b.wiggleFrequency
|
||||
&& a.tumbleSpeed === b.tumbleSpeed
|
||||
&& a.tumbleAmplitude === b.tumbleAmplitude
|
||||
&& a.tumbleFrequency === b.tumbleFrequency;
|
||||
}
|
||||
|
||||
export function createAnimationValues(props: AnimationProps) {
|
||||
return {
|
||||
uWiggleSpeed: ValueCell.create(props.wiggleSpeed),
|
||||
uWiggleAmplitude: ValueCell.create(props.wiggleAmplitude),
|
||||
uWiggleFrequency: ValueCell.create(props.wiggleFrequency),
|
||||
uWiggleMode: ValueCell.create(props.wiggleMode === 'position' ? 0 : 1),
|
||||
uTumbleSpeed: ValueCell.create(props.tumbleSpeed),
|
||||
uTumbleAmplitude: ValueCell.create(props.tumbleAmplitude),
|
||||
uTumbleFrequency: ValueCell.create(props.tumbleFrequency),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateAnimationValues(values: AnimationData, props: AnimationProps) {
|
||||
ValueCell.updateIfChanged(values.uWiggleSpeed, props.wiggleSpeed);
|
||||
ValueCell.updateIfChanged(values.uWiggleAmplitude, props.wiggleAmplitude);
|
||||
ValueCell.updateIfChanged(values.uWiggleFrequency, props.wiggleFrequency);
|
||||
ValueCell.updateIfChanged(values.uWiggleMode, props.wiggleMode === 'position' ? 0 : 1);
|
||||
ValueCell.updateIfChanged(values.uTumbleSpeed, props.tumbleSpeed);
|
||||
ValueCell.updateIfChanged(values.uTumbleAmplitude, props.tumbleAmplitude);
|
||||
ValueCell.updateIfChanged(values.uTumbleFrequency, props.tumbleFrequency);
|
||||
}
|
||||
@@ -72,6 +72,25 @@ export function getColorSmoothingProps(smoothColors: PD.Values<ColorSmoothingPar
|
||||
|
||||
//
|
||||
|
||||
export type InstanceGranularityValue = true | false | 'auto'
|
||||
export const InstanceGranularityOptions: [InstanceGranularityValue, string][] = [[true, 'On'], [false, 'Off'], ['auto', 'Auto']];
|
||||
|
||||
/**
|
||||
* Threshold (in `groupCount * instanceCount`, e.g. number of marker-texture
|
||||
* slots) above which `instanceGranularity: 'auto'` resolves to `true`.
|
||||
*/
|
||||
export const AutoInstanceGranularityThreshold = 50_000_000;
|
||||
|
||||
/**
|
||||
* Resolves the `instanceGranularity` param value to a boolean.
|
||||
*/
|
||||
export function resolveInstanceGranularity(value: InstanceGranularityValue, groupCount: number, instanceCount: number): boolean {
|
||||
if (value === 'auto') return groupCount * instanceCount > AutoInstanceGranularityThreshold;
|
||||
return value;
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export namespace BaseGeometry {
|
||||
export const MaterialCategory: PD.Info = { category: 'Material' };
|
||||
export const ShadingCategory: PD.Info = { category: 'Shading' };
|
||||
@@ -88,7 +107,7 @@ export namespace BaseGeometry {
|
||||
clip: PD.Group(Clip.Params),
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
|
||||
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
|
||||
instanceGranularity: PD.Select<InstanceGranularityValue>('auto', InstanceGranularityOptions, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory. When set to `auto`, granularity is enabled if `groupCount * instanceCount` exceeds `AutoInstanceGranularityThreshold`.' }),
|
||||
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
|
||||
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
|
||||
batchSize: PD.Numeric(2000, { min: 0, max: 50000, step: 500 }, { ...CullingLodCategory, description: 'Instance grid batch size.' }),
|
||||
@@ -130,7 +149,7 @@ export namespace BaseGeometry {
|
||||
uClipObjectScale: ValueCell.create(clip.objects.scale),
|
||||
uClipObjectTransform: ValueCell.create(clip.objects.transform),
|
||||
|
||||
instanceGranularity: ValueCell.create(props.instanceGranularity),
|
||||
instanceGranularity: ValueCell.create(resolveInstanceGranularity(props.instanceGranularity, counts.groupCount, counts.instanceCount)),
|
||||
uLod: ValueCell.create(Vec4.create(props.lod[0], props.lod[1], props.lod[2], 0)),
|
||||
};
|
||||
}
|
||||
@@ -153,7 +172,7 @@ export namespace BaseGeometry {
|
||||
ValueCell.update(values.uClipObjectScale, clip.objects.scale);
|
||||
ValueCell.update(values.uClipObjectTransform, clip.objects.transform);
|
||||
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, props.instanceGranularity);
|
||||
ValueCell.updateIfChanged(values.instanceGranularity, resolveInstanceGranularity(props.instanceGranularity, values.uGroupCount.ref.value, values.instanceCount.ref.value));
|
||||
ValueCell.update(values.uLod, Vec4.set(values.uLod.ref.value, props.lod[0], props.lod[1], props.lod[2], 0));
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -28,7 +28,9 @@ import { CylindersValues } from '../../../mol-gl/renderable/cylinders';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getInteriorParam, updateInteriorValues, createInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Cylinders {
|
||||
readonly kind: 'cylinders',
|
||||
@@ -180,6 +182,7 @@ export namespace Cylinders {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
colorMode: PD.Select('default', PD.arrayToOptions(['default', 'interpolate'] as const), BaseGeometry.ShadingCategory)
|
||||
};
|
||||
export type Params = typeof Params
|
||||
@@ -222,7 +225,7 @@ export namespace Cylinders {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -230,6 +233,7 @@ export namespace Cylinders {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: cylinders.cylinderCount * 4 * 3, vertexCount: cylinders.cylinderCount * 6, groupCount, instanceCount };
|
||||
|
||||
@@ -258,6 +262,7 @@ export namespace Cylinders {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
padding: ValueCell.create(padding),
|
||||
@@ -272,9 +277,10 @@ export namespace Cylinders {
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
dDualColor: ValueCell.create(props.colorMode === 'interpolate'),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -295,9 +301,9 @@ export namespace Cylinders {
|
||||
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
ValueCell.updateIfChanged(values.dDualColor, props.colorMode === 'interpolate');
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: CylindersValues, cylinders: Cylinders) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Box } from '../../primitive/box';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -29,6 +29,7 @@ import { createEmptyClipping } from '../clipping-data';
|
||||
import { Grid } from '../../../mol-model/volume';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
const VolumeBox = Box();
|
||||
|
||||
@@ -227,7 +228,7 @@ export namespace DirectVolume {
|
||||
const positionIt = createPositionIterator(directVolume, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -235,6 +236,7 @@ export namespace DirectVolume {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const [x, y, z] = gridDimension.ref.value;
|
||||
const counts = { drawCount: VolumeBox.indices.length, vertexCount: x * y * z, groupCount, instanceCount };
|
||||
@@ -255,6 +257,7 @@ export namespace DirectVolume {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createColors } from '../color-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { createMarkers } from '../marker-data';
|
||||
@@ -28,6 +28,7 @@ import { NullLocation } from '../../../mol-model/location';
|
||||
import { QuadPositions } from '../../../mol-gl/compute/util';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
const QuadIndices = new Uint32Array([
|
||||
0, 1, 2,
|
||||
@@ -74,6 +75,8 @@ interface Image {
|
||||
|
||||
setBoundingSphere(boundingSphere: Sphere3D): void
|
||||
hasBoundingSphere(): boolean
|
||||
|
||||
readonly meta: { [k: string]: unknown }
|
||||
}
|
||||
|
||||
namespace Image {
|
||||
@@ -136,7 +139,8 @@ namespace Image {
|
||||
},
|
||||
hasBoundingSphere() {
|
||||
return currentHash === hashCode(image);
|
||||
}
|
||||
},
|
||||
meta: {}
|
||||
};
|
||||
return image;
|
||||
}
|
||||
@@ -197,7 +201,7 @@ namespace Image {
|
||||
const positionIt = createPositionIterator(image, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -205,6 +209,7 @@ namespace Image {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: QuadIndices.length, vertexCount: QuadPositions.length / 3, groupCount, instanceCount };
|
||||
|
||||
@@ -221,6 +226,7 @@ namespace Image {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2025-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -8,6 +8,12 @@ import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { Material } from '../../mol-util/material';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
|
||||
export type InteriorData = {
|
||||
uInteriorColor: ValueCell<Vec4>,
|
||||
uInteriorSubstance: ValueCell<Vec4>,
|
||||
}
|
||||
|
||||
export function getInteriorParam() {
|
||||
return PD.Group({
|
||||
@@ -17,23 +23,36 @@ export function getInteriorParam() {
|
||||
substanceStrength: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
});
|
||||
}
|
||||
export type InteriorProp = ReturnType<typeof getInteriorParam>['defaultValue'];
|
||||
export type InteriorParam = ReturnType<typeof getInteriorParam>
|
||||
export type InteriorProps = InteriorParam['defaultValue'];
|
||||
|
||||
export function areInteriorPropsEquals(a: InteriorProp, b: InteriorProp): boolean {
|
||||
export function areInteriorPropsEquals(a: InteriorProps, b: InteriorProps): boolean {
|
||||
return a.color === b.color
|
||||
&& a.colorStrength === b.colorStrength
|
||||
&& Material.areEqual(a.substance, b.substance)
|
||||
&& a.substanceStrength === b.substanceStrength;
|
||||
}
|
||||
|
||||
export function getInteriorColor(props: InteriorProp, out: Vec4): Vec4 {
|
||||
export function getInteriorColor(props: InteriorProps, out: Vec4): Vec4 {
|
||||
Color.toArrayNormalized(props.color, out, 0);
|
||||
out[3] = props.colorStrength;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function getInteriorSubstance(props: InteriorProp, out: Vec4): Vec4 {
|
||||
export function getInteriorSubstance(props: InteriorProps, out: Vec4): Vec4 {
|
||||
Material.toArrayNormalized(props.substance, out, 0);
|
||||
out[3] = props.substanceStrength;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function createInteriorValues(props: InteriorProps) {
|
||||
return {
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props, Vec4())),
|
||||
};
|
||||
}
|
||||
|
||||
export function updateInteriorValues(values: InteriorData, props: InteriorProps) {
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props, values.uInteriorSubstance.ref.value));
|
||||
}
|
||||
|
||||
51
src/mol-geo/geometry/lines/builder/box.ts
Normal file
51
src/mol-geo/geometry/lines/builder/box.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _start = Vec3();
|
||||
const _end = Vec3();
|
||||
|
||||
// 8 corners of the unit cube [0,1]^3: indexed by 3 bits (x=bit0, y=bit1, z=bit2)
|
||||
const _corners: Vec3[] = [];
|
||||
for (let i = 0; i < 8; ++i) _corners.push(Vec3());
|
||||
|
||||
// 12 box edges: [startCorner, endCorner, axis]
|
||||
// axis: 0=x, 1=y, 2=z — the axis along which the edge runs
|
||||
const BoxEdges: [number, number, number][] = [
|
||||
// X-axis edges (y,z vary)
|
||||
[0, 1, 0], [2, 3, 0], [4, 5, 0], [6, 7, 0],
|
||||
// Y-axis edges (x,z vary)
|
||||
[0, 2, 1], [1, 3, 1], [4, 6, 1], [5, 7, 1],
|
||||
// Z-axis edges (x,y vary)
|
||||
[0, 4, 2], [1, 5, 2], [2, 6, 2], [3, 7, 2],
|
||||
];
|
||||
|
||||
/**
|
||||
* Add a wireframe box to a LinesBuilder.
|
||||
*/
|
||||
export function addBox(builder: LinesBuilder, transform: Mat4, group: number) {
|
||||
// Compute 8 corners in world space from unit cube [0,1]^3
|
||||
// Corner index bits: bit0=x(0/1), bit1=y(0/1), bit2=z(0/1)
|
||||
for (let ci = 0; ci < 8; ++ci) {
|
||||
Vec3.set(_corners[ci],
|
||||
(ci & 1) ? 1 : 0,
|
||||
(ci & 2) ? 1 : 0,
|
||||
(ci & 4) ? 1 : 0,
|
||||
);
|
||||
Vec3.transformMat4(_corners[ci], _corners[ci], transform);
|
||||
}
|
||||
|
||||
for (const [si, ei] of BoxEdges) {
|
||||
Vec3.copy(_start, _corners[si]);
|
||||
Vec3.copy(_end, _corners[ei]);
|
||||
|
||||
// Draw edge line
|
||||
builder.addVec(_start, _end, group);
|
||||
}
|
||||
}
|
||||
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal file
34
src/mol-geo/geometry/lines/builder/plane.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _c0 = Vec3();
|
||||
const _c1 = Vec3();
|
||||
const _c2 = Vec3();
|
||||
const _c3 = Vec3();
|
||||
|
||||
/**
|
||||
* Add wireframe edges of a quad to a LinesBuilder.
|
||||
*/
|
||||
export function addPlane(builder: LinesBuilder, corners: ArrayLike<number>, transform: Mat4, group: number) {
|
||||
Vec3.fromArray(_c0, corners, 0);
|
||||
Vec3.fromArray(_c1, corners, 3);
|
||||
Vec3.fromArray(_c2, corners, 6);
|
||||
Vec3.fromArray(_c3, corners, 9);
|
||||
|
||||
Vec3.transformMat4(_c0, _c0, transform);
|
||||
Vec3.transformMat4(_c1, _c1, transform);
|
||||
Vec3.transformMat4(_c2, _c2, transform);
|
||||
Vec3.transformMat4(_c3, _c3, transform);
|
||||
|
||||
builder.addVec(_c0, _c1, group);
|
||||
builder.addVec(_c1, _c2, group);
|
||||
builder.addVec(_c2, _c3, group);
|
||||
builder.addVec(_c3, _c0, group);
|
||||
}
|
||||
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal file
82
src/mol-geo/geometry/lines/builder/sphere.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { LinesBuilder } from '../lines-builder';
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra/3d/vec3';
|
||||
import { Mat4 } from '../../../../mol-math/linear-algebra/3d/mat4';
|
||||
|
||||
const _p0 = Vec3();
|
||||
const _p1 = Vec3();
|
||||
|
||||
export interface AddSphereOptions {
|
||||
/** Number of segments per circle (default: 32) */
|
||||
segments?: number,
|
||||
/** Number of circles per dimension, evenly spaced along each axis (default: 1) */
|
||||
circlesPerDimension?: number,
|
||||
}
|
||||
|
||||
const DefaultAddSphereOptions: Required<AddSphereOptions> = {
|
||||
segments: 32,
|
||||
circlesPerDimension: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a wireframe sphere to a LinesBuilder as orthogonal circles in XY, XZ, YZ planes.
|
||||
*/
|
||||
export function addSphere(builder: LinesBuilder, radius: number, transform: Mat4, group: number, options?: AddSphereOptions) {
|
||||
const segments = options?.segments ?? DefaultAddSphereOptions.segments;
|
||||
const circlesPerDim = options?.circlesPerDimension ?? DefaultAddSphereOptions.circlesPerDimension;
|
||||
|
||||
// For each dimension, draw `circlesPerDim` circles spaced evenly along the axis.
|
||||
// circlesPerDim=1 → one circle at the center (offset=0)
|
||||
// circlesPerDim=3 → circles at -r/2, 0, +r/2
|
||||
|
||||
for (let dim = 0; dim < 3; ++dim) {
|
||||
for (let ci = 0; ci < circlesPerDim; ++ci) {
|
||||
// offset along the perpendicular axis: evenly spaced in [-radius, radius]
|
||||
const offset = circlesPerDim === 1
|
||||
? 0
|
||||
: -radius + (2 * radius * (ci + 1)) / (circlesPerDim + 1);
|
||||
|
||||
// Choose a smaller radius for offset circles (cross-section of the sphere)
|
||||
const r = Math.sqrt(Math.max(0, radius * radius - offset * offset));
|
||||
if (r < 1e-8) continue;
|
||||
|
||||
addCircle(builder, r, offset, dim, transform, group, segments);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addCircle(builder: LinesBuilder, radius: number, offset: number, perpAxis: number, transform: Mat4, group: number, segments: number) {
|
||||
// perpAxis: 0=X (circle in YZ), 1=Y (circle in XZ), 2=Z (circle in XY)
|
||||
for (let i = 0; i < segments; ++i) {
|
||||
const a0 = (i / segments) * Math.PI * 2;
|
||||
const a1 = ((i + 1) / segments) * Math.PI * 2;
|
||||
|
||||
const cos0 = Math.cos(a0) * radius;
|
||||
const sin0 = Math.sin(a0) * radius;
|
||||
const cos1 = Math.cos(a1) * radius;
|
||||
const sin1 = Math.sin(a1) * radius;
|
||||
|
||||
if (perpAxis === 2) {
|
||||
// XY circle at z=offset
|
||||
Vec3.set(_p0, cos0, sin0, offset);
|
||||
Vec3.set(_p1, cos1, sin1, offset);
|
||||
} else if (perpAxis === 1) {
|
||||
// XZ circle at y=offset
|
||||
Vec3.set(_p0, cos0, offset, sin0);
|
||||
Vec3.set(_p1, cos1, offset, sin1);
|
||||
} else {
|
||||
// YZ circle at x=offset
|
||||
Vec3.set(_p0, offset, cos0, sin0);
|
||||
Vec3.set(_p1, offset, cos1, sin1);
|
||||
}
|
||||
|
||||
Vec3.transformMat4(_p0, _p0, transform);
|
||||
Vec3.transformMat4(_p1, _p1, transform);
|
||||
builder.addVec(_p0, _p1, group);
|
||||
}
|
||||
}
|
||||
@@ -21,13 +21,15 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
/** Wide line */
|
||||
export interface Lines {
|
||||
@@ -188,6 +190,7 @@ export namespace Lines {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(2, { min: 0, max: 10, step: 0.1 }),
|
||||
lineSizeAttenuation: PD.Boolean(false),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -229,7 +232,7 @@ export namespace Lines {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -237,6 +240,7 @@ export namespace Lines {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: lines.lineCount * 2 * 3, vertexCount: lines.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -262,6 +266,7 @@ export namespace Lines {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -269,6 +274,7 @@ export namespace Lines {
|
||||
dLineSizeAttenuation: ValueCell.create(props.lineSizeAttenuation),
|
||||
uDoubleSided: ValueCell.create(true),
|
||||
dFlipSided: ValueCell.create(false),
|
||||
...createAnimationValues(props.animation),
|
||||
|
||||
stripCount: lines.stripCount,
|
||||
stripOffsets: lines.stripBuffer,
|
||||
@@ -285,6 +291,7 @@ export namespace Lines {
|
||||
BaseGeometry.updateValues(values, props);
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.dLineSizeAttenuation, props.lineSizeAttenuation);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: LinesValues, lines: Lines) {
|
||||
|
||||
@@ -20,7 +20,7 @@ import { calculateInvariantBoundingSphere, calculateTransformBoundingSphere } fr
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { MeshValues } from '../../../mol-gl/renderable/mesh';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
@@ -29,7 +29,9 @@ import { arraySetAdd } from '../../../mol-util/array';
|
||||
import { degToRad } from '../../../mol-math/misc';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Mesh {
|
||||
readonly kind: 'mesh',
|
||||
@@ -639,6 +641,7 @@ export namespace Mesh {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -681,7 +684,7 @@ export namespace Mesh {
|
||||
const positionIt = createPositionIterator(mesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -689,6 +692,7 @@ export namespace Mesh {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: mesh.triangleCount * 3, vertexCount: mesh.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -713,6 +717,7 @@ export namespace Mesh {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -725,10 +730,11 @@ export namespace Mesh {
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
meta: ValueCell.create(mesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -749,8 +755,8 @@ export namespace Mesh {
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: MeshValues, mesh: Mesh) {
|
||||
|
||||
@@ -20,13 +20,15 @@ import { Theme } from '../../../mol-theme/theme';
|
||||
import { PointsValues } from '../../../mol-gl/renderable/points';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
/** Point cloud */
|
||||
export interface Points {
|
||||
@@ -136,6 +138,7 @@ export namespace Points {
|
||||
sizeFactor: PD.Numeric(3, { min: 0, max: 10, step: 0.1 }),
|
||||
pointSizeAttenuation: PD.Boolean(false),
|
||||
pointStyle: PD.Select('square', PD.objectToOptions(StyleTypes)),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -175,7 +178,7 @@ export namespace Points {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -183,6 +186,7 @@ export namespace Points {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: points.pointCount, vertexCount: points.pointCount, groupCount, instanceCount };
|
||||
|
||||
@@ -205,12 +209,14 @@ export namespace Points {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor),
|
||||
dPointSizeAttenuation: ValueCell.create(props.pointSizeAttenuation),
|
||||
dPointStyle: ValueCell.create(props.pointStyle),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -225,6 +231,7 @@ export namespace Points {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.dPointSizeAttenuation, props.pointSizeAttenuation);
|
||||
ValueCell.updateIfChanged(values.dPointStyle, props.pointStyle);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: PointsValues, points: Points) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { TextureImage, calculateInvariantBoundingSphere, calculateTransformBound
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { createSizes, getMaxSize } from '../size-data';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -27,7 +27,9 @@ import { Vec2, Vec3, Vec4 } from '../../../mol-math/linear-algebra';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface Spheres {
|
||||
readonly kind: 'spheres',
|
||||
@@ -247,6 +249,33 @@ export namespace Spheres {
|
||||
return lodLevels.map(l => getAdjustedStride(l, sizeFactor)).reverse();
|
||||
}
|
||||
|
||||
export const LodLevelsPresets: { [key in 'performance' | 'balanced' | 'quality' | 'ultra']: LodLevels } = {
|
||||
performance: [
|
||||
{ minDistance: 1, maxDistance: 300, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 300, maxDistance: 2000, overlap: 0, stride: 40, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 150, scaleBias: 3 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 300, scaleBias: 2.5 },
|
||||
],
|
||||
balanced: [
|
||||
{ minDistance: 1, maxDistance: 500, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 500, maxDistance: 2000, overlap: 0, stride: 15, scaleBias: 3 },
|
||||
{ minDistance: 2000, maxDistance: 6000, overlap: 0, stride: 70, scaleBias: 2.7 },
|
||||
{ minDistance: 6000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.5 },
|
||||
],
|
||||
quality: [
|
||||
{ minDistance: 1, maxDistance: 1000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 1000, maxDistance: 4000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 4000, maxDistance: 10000, overlap: 0, stride: 50, scaleBias: 2.7 },
|
||||
{ minDistance: 10000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2.3 },
|
||||
],
|
||||
ultra: [
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
],
|
||||
};
|
||||
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
@@ -262,6 +291,7 @@ export namespace Spheres {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
lodLevels: PD.ObjectList({
|
||||
minDistance: PD.Numeric(0),
|
||||
maxDistance: PD.Numeric(0),
|
||||
@@ -270,7 +300,8 @@ export namespace Spheres {
|
||||
scaleBias: PD.Numeric(3, { min: 0.1, max: 10, step: 0.1 }),
|
||||
}, o => `${o.stride}`, {
|
||||
...BaseGeometry.CullingLodCategory,
|
||||
defaultValue: [] as LodLevels
|
||||
defaultValue: [] as LodLevels,
|
||||
presets: Object.entries(LodLevelsPresets).map(([k, v]) => [v, k])
|
||||
})
|
||||
};
|
||||
export type Params = typeof Params
|
||||
@@ -311,7 +342,7 @@ export namespace Spheres {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -319,6 +350,7 @@ export namespace Spheres {
|
||||
const emissive = createEmptyEmissive();
|
||||
const material = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: spheres.sphereCount * 2 * 3, vertexCount: spheres.sphereCount * 6, groupCount, instanceCount };
|
||||
|
||||
@@ -345,6 +377,7 @@ export namespace Spheres {
|
||||
...emissive,
|
||||
...material,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
padding: ValueCell.create(padding),
|
||||
@@ -362,12 +395,13 @@ export namespace Spheres {
|
||||
uAlphaThickness: ValueCell.create(props.alphaThickness),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
lodLevels: spheres.shaderData.lodLevels,
|
||||
centerBuffer: spheres.centerBuffer,
|
||||
groupBuffer: spheres.groupBuffer,
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -391,8 +425,8 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uAlphaThickness, props.alphaThickness);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
|
||||
const lodLevels = getLodLevels(values.lodLevels.ref.value as LodLevelsValue);
|
||||
if (!areLodLevelsEqual(props.lodLevels, lodLevels)) {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { FontAtlasParams } from './font-atlas';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { clamp } from '../../../mol-math/interpolate';
|
||||
import { createRenderObject as _createRenderObject } from '../../../mol-gl/render-object';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
@@ -33,6 +33,7 @@ import { GroupMapping, createGroupMapping } from '../../util';
|
||||
import { createEmptyClipping } from '../clipping-data';
|
||||
import { createEmptySubstance } from '../substance-data';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
|
||||
type TextAttachment = (
|
||||
'bottom-left' | 'bottom-center' | 'bottom-right' |
|
||||
@@ -218,7 +219,7 @@ export namespace Text {
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const size = createSizes(locationIt, positionIt, theme.size);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -226,6 +227,7 @@ export namespace Text {
|
||||
const emissive = createEmptyEmissive();
|
||||
const substance = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: text.charCount * 2 * 3, vertexCount: text.charCount * 4, groupCount, instanceCount };
|
||||
|
||||
@@ -253,6 +255,7 @@ export namespace Text {
|
||||
...emissive,
|
||||
...substance,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
aTexCoord: text.tcoordBuffer,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Cai Huiyu <szmun.caihy@gmail.com>
|
||||
@@ -15,7 +15,7 @@ import { createMarkers } from '../marker-data';
|
||||
import { GeometryUtils } from '../geometry';
|
||||
import { Theme } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { BaseGeometry } from '../base';
|
||||
import { BaseGeometry, resolveInstanceGranularity } from '../base';
|
||||
import { createEmptyOverpaint } from '../overpaint-data';
|
||||
import { createEmptyTransparency } from '../transparency-data';
|
||||
import { TextureMeshValues } from '../../../mol-gl/renderable/texture-mesh';
|
||||
@@ -28,7 +28,9 @@ import { createEmptySubstance } from '../substance-data';
|
||||
import { RenderableState } from '../../../mol-gl/renderable';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { createEmptyEmissive } from '../emissive-data';
|
||||
import { getInteriorColor, getInteriorParam, getInteriorSubstance } from '../interior';
|
||||
import { createEmptyWiggle } from '../wiggle-data';
|
||||
import { createInteriorValues, getInteriorParam, updateInteriorValues } from '../interior';
|
||||
import { getAnimationParam, createAnimationValues, updateAnimationValues } from '../animation';
|
||||
|
||||
export interface TextureMesh {
|
||||
readonly kind: 'texture-mesh',
|
||||
@@ -130,6 +132,7 @@ export namespace TextureMesh {
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
bumpAmplitude: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
interior: getInteriorParam(),
|
||||
animation: getAnimationParam(),
|
||||
};
|
||||
export type Params = typeof Params
|
||||
|
||||
@@ -200,7 +203,7 @@ export namespace TextureMesh {
|
||||
const positionIt = Utils.createPositionIterator(textureMesh, transform);
|
||||
|
||||
const color = createColors(locationIt, positionIt, theme.color);
|
||||
const marker = props.instanceGranularity
|
||||
const marker = resolveInstanceGranularity(props.instanceGranularity, groupCount, instanceCount)
|
||||
? createMarkers(instanceCount, 'instance')
|
||||
: createMarkers(instanceCount * groupCount, 'groupInstance');
|
||||
const overpaint = createEmptyOverpaint();
|
||||
@@ -208,6 +211,7 @@ export namespace TextureMesh {
|
||||
const emissive = createEmptyEmissive();
|
||||
const substance = createEmptySubstance();
|
||||
const clipping = createEmptyClipping();
|
||||
const wiggle = createEmptyWiggle();
|
||||
|
||||
const counts = { drawCount: textureMesh.vertexCount, vertexCount: textureMesh.vertexCount, groupCount, instanceCount };
|
||||
|
||||
@@ -234,6 +238,7 @@ export namespace TextureMesh {
|
||||
...emissive,
|
||||
...substance,
|
||||
...clipping,
|
||||
...wiggle,
|
||||
...transform,
|
||||
|
||||
...BaseGeometry.createValues(props, counts),
|
||||
@@ -246,10 +251,11 @@ export namespace TextureMesh {
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
uBumpAmplitude: ValueCell.create(props.bumpAmplitude),
|
||||
uInteriorColor: ValueCell.create(getInteriorColor(props.interior, Vec4())),
|
||||
uInteriorSubstance: ValueCell.create(getInteriorSubstance(props.interior, Vec4())),
|
||||
|
||||
meta: ValueCell.create(textureMesh.meta),
|
||||
|
||||
...createInteriorValues(props.interior),
|
||||
...createAnimationValues(props.animation),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,8 +276,8 @@ export namespace TextureMesh {
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
ValueCell.updateIfChanged(values.uBumpAmplitude, props.bumpAmplitude);
|
||||
ValueCell.update(values.uInteriorColor, getInteriorColor(props.interior, values.uInteriorColor.ref.value));
|
||||
ValueCell.update(values.uInteriorSubstance, getInteriorSubstance(props.interior, values.uInteriorSubstance.ref.value));
|
||||
updateInteriorValues(values, props.interior);
|
||||
updateAnimationValues(values, props.animation);
|
||||
}
|
||||
|
||||
function updateBoundingSphere(values: TextureMeshValues, textureMesh: TextureMesh) {
|
||||
|
||||
79
src/mol-geo/geometry/wiggle-data.ts
Normal file
79
src/mol-geo/geometry/wiggle-data.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { TextureImage, createTextureImage } from '../../mol-gl/renderable/util';
|
||||
|
||||
export type WiggleType = 'instance' | 'groupInstance';
|
||||
|
||||
export type WiggleData = {
|
||||
tWiggle: ValueCell<TextureImage<Uint8Array>>
|
||||
uWiggleTexDim: ValueCell<Vec2>
|
||||
dWiggle: ValueCell<boolean>,
|
||||
wiggleAverage: ValueCell<number>,
|
||||
dWiggleType: ValueCell<string>,
|
||||
uWiggleStrength: ValueCell<number>,
|
||||
}
|
||||
|
||||
export function applyWiggleValue(array: Uint8Array, start: number, end: number, value: number) {
|
||||
for (let i = start; i < end; ++i) {
|
||||
array[i] = value * 255;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function getWiggleAverage(array: Uint8Array, count: number): number {
|
||||
if (count === 0 || array.length < count) return 0;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < count; ++i) {
|
||||
sum += array[i];
|
||||
}
|
||||
return sum / (255 * count);
|
||||
}
|
||||
|
||||
export function clearWiggle(array: Uint8Array, start: number, end: number) {
|
||||
array.fill(0, start, end);
|
||||
}
|
||||
|
||||
export function createWiggle(count: number, type: WiggleType, wiggleData?: WiggleData): WiggleData {
|
||||
const wiggle = createTextureImage(Math.max(1, count), 1, Uint8Array, wiggleData && wiggleData.tWiggle.ref.value.array);
|
||||
if (wiggleData) {
|
||||
ValueCell.update(wiggleData.tWiggle, wiggle);
|
||||
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(wiggle.width, wiggle.height));
|
||||
ValueCell.updateIfChanged(wiggleData.dWiggle, count > 0);
|
||||
ValueCell.updateIfChanged(wiggleData.wiggleAverage, getWiggleAverage(wiggle.array, count));
|
||||
ValueCell.updateIfChanged(wiggleData.dWiggleType, type);
|
||||
return wiggleData;
|
||||
} else {
|
||||
return {
|
||||
tWiggle: ValueCell.create(wiggle),
|
||||
uWiggleTexDim: ValueCell.create(Vec2.create(wiggle.width, wiggle.height)),
|
||||
dWiggle: ValueCell.create(count > 0),
|
||||
wiggleAverage: ValueCell.create(0),
|
||||
dWiggleType: ValueCell.create(type),
|
||||
uWiggleStrength: ValueCell.create(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const emptyWiggleTexture = { array: new Uint8Array(1), width: 1, height: 1 };
|
||||
export function createEmptyWiggle(wiggleData?: WiggleData): WiggleData {
|
||||
if (wiggleData) {
|
||||
ValueCell.update(wiggleData.tWiggle, emptyWiggleTexture);
|
||||
ValueCell.update(wiggleData.uWiggleTexDim, Vec2.create(1, 1));
|
||||
return wiggleData;
|
||||
} else {
|
||||
return {
|
||||
tWiggle: ValueCell.create(emptyWiggleTexture),
|
||||
uWiggleTexDim: ValueCell.create(Vec2.create(1, 1)),
|
||||
dWiggle: ValueCell.create(false),
|
||||
wiggleAverage: ValueCell.create(0),
|
||||
dWiggleType: ValueCell.create('groupInstance'),
|
||||
uWiggleStrength: ValueCell.create(1),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ describe('renderer', () => {
|
||||
scene.add(points);
|
||||
scene.commit();
|
||||
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(10);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(11);
|
||||
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 5 : 0);
|
||||
expect(ctx.stats.resourceCounts.program).toBe(5);
|
||||
expect(ctx.stats.resourceCounts.shader).toBe(10);
|
||||
@@ -89,7 +89,7 @@ describe('renderer', () => {
|
||||
sceneDpoit.commit();
|
||||
|
||||
expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 12 : 15);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(28);
|
||||
expect(ctx.stats.resourceCounts.texture).toBe(31);
|
||||
expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 15 : 0);
|
||||
expect(ctx.stats.resourceCounts.program).toBe(7);
|
||||
expect(ctx.stats.resourceCounts.shader).toBe(14);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, Values, InternalSchema, SizeSchema, InternalValues, ElementsSpec, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { CylindersShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -32,9 +32,10 @@ export const CylindersSchema = {
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
dDualColor: DefineSpec('boolean'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type CylindersSchema = typeof CylindersSchema
|
||||
export type CylindersValues = Values<CylindersSchema>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, ValueSpec } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, ElementsSpec, InternalValues, GlobalTextureSchema, UniformSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, ValueSpec, AnimationSchema } from './schema';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { LinesShaderCode } from '../shader-code';
|
||||
|
||||
@@ -24,6 +24,8 @@ export const LinesSchema = {
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
stripCount: ValueSpec('number'),
|
||||
stripOffsets: ValueSpec('uint32'),
|
||||
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type LinesSchema = typeof LinesSchema
|
||||
export type LinesValues = Values<LinesSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, ElementsSpec, DefineSpec, Values, InternalSchema, InternalValues, GlobalTextureSchema, ValueSpec, UniformSpec, GlobalDefineSchema, GlobalDefineValues, GlobalDefines, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { MeshShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -27,9 +27,10 @@ export const MeshSchema = {
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
meta: ValueSpec('unknown')
|
||||
meta: ValueSpec('unknown'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
} as const;
|
||||
export type MeshSchema = typeof MeshSchema
|
||||
export type MeshValues = Values<MeshSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, AttributeSpec, DefineSpec, Values, InternalSchema, SizeSchema, InternalValues, GlobalTextureSchema, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, AnimationSchema } from './schema';
|
||||
import { PointsShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -18,6 +18,8 @@ export const PointsSchema = {
|
||||
aPosition: AttributeSpec('float32', 3, 0),
|
||||
dPointSizeAttenuation: DefineSpec('boolean'),
|
||||
dPointStyle: DefineSpec('string', ['square', 'circle', 'fuzzy']),
|
||||
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type PointsSchema = typeof PointsSchema
|
||||
export type PointsValues = Values<PointsSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -179,6 +179,9 @@ export const GlobalUniformSchema = {
|
||||
uMarkingDepthTest: UniformSpec('b'),
|
||||
uMarkingType: UniformSpec('i'),
|
||||
uPickType: UniformSpec('i'),
|
||||
|
||||
uTime: UniformSpec('f'),
|
||||
uEnableAnimation: UniformSpec('b'),
|
||||
} as const;
|
||||
export type GlobalUniformSchema = typeof GlobalUniformSchema
|
||||
export type GlobalUniformValues = Values<GlobalUniformSchema>
|
||||
@@ -315,6 +318,17 @@ export const ClippingSchema = {
|
||||
export type ClippingSchema = typeof ClippingSchema
|
||||
export type ClippingValues = Values<ClippingSchema>
|
||||
|
||||
export const WiggleSchema = {
|
||||
uWiggleTexDim: UniformSpec('v2'),
|
||||
tWiggle: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
|
||||
dWiggle: DefineSpec('boolean'),
|
||||
wiggleAverage: ValueSpec('number'),
|
||||
dWiggleType: DefineSpec('string', ['instance', 'groupInstance']),
|
||||
uWiggleStrength: UniformSpec('f', 'material'),
|
||||
} as const;
|
||||
export type WiggleSchema = typeof WiggleSchema
|
||||
export type WiggleValues = Values<WiggleSchema>
|
||||
|
||||
export const BaseSchema = {
|
||||
dGeometryType: DefineSpec('string', ['cylinders', 'directVolume', 'image', 'lines', 'mesh', 'points', 'spheres', 'text', 'textureMesh']),
|
||||
|
||||
@@ -325,6 +339,7 @@ export const BaseSchema = {
|
||||
...EmissiveSchema,
|
||||
...SubstanceSchema,
|
||||
...ClippingSchema,
|
||||
...WiggleSchema,
|
||||
|
||||
dClipObjectCount: DefineSpec('number'),
|
||||
dClipVariant: DefineSpec('string', ['instance', 'pixel']),
|
||||
@@ -386,3 +401,24 @@ export const BaseSchema = {
|
||||
} as const;
|
||||
export type BaseSchema = typeof BaseSchema
|
||||
export type BaseValues = Values<BaseSchema>
|
||||
|
||||
//
|
||||
|
||||
export const InteriorSchema = {
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
} as const;
|
||||
export type InteriorSchema = typeof InteriorSchema
|
||||
export type InteriorValues = Values<InteriorSchema>
|
||||
|
||||
export const AnimationSchema = {
|
||||
uWiggleSpeed: UniformSpec('f', 'material'),
|
||||
uWiggleAmplitude: UniformSpec('f', 'material'),
|
||||
uWiggleFrequency: UniformSpec('f', 'material'),
|
||||
uWiggleMode: UniformSpec('i', 'material'),
|
||||
uTumbleSpeed: UniformSpec('f', 'material'),
|
||||
uTumbleAmplitude: UniformSpec('f', 'material'),
|
||||
uTumbleFrequency: UniformSpec('f', 'material'),
|
||||
} as const;
|
||||
export type AnimationSchema = typeof AnimationSchema
|
||||
export type AnimationValues = Values<AnimationSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, Values, InternalSchema, SizeSchema, InternalValues, ValueSpec, DefineSpec, GlobalTextureSchema, UniformSpec, TextureSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { SpheresShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -30,12 +30,13 @@ export const SpheresSchema = {
|
||||
uAlphaThickness: UniformSpec('f'),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
|
||||
lodLevels: ValueSpec('unknown'),
|
||||
centerBuffer: ValueSpec('float32'),
|
||||
groupBuffer: ValueSpec('float32'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type SpheresSchema = typeof SpheresSchema
|
||||
export type SpheresValues = Values<SpheresSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -7,7 +7,7 @@
|
||||
import { Renderable, RenderableState, createRenderable } from '../renderable';
|
||||
import { WebGLContext } from '../webgl/context';
|
||||
import { createGraphicsRenderItem, Transparency } from '../webgl/render-item';
|
||||
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema } from './schema';
|
||||
import { GlobalUniformSchema, BaseSchema, DefineSpec, Values, InternalSchema, InternalValues, UniformSpec, TextureSpec, GlobalTextureSchema, ValueSpec, GlobalDefineValues, GlobalDefines, GlobalDefineSchema, InteriorSchema, AnimationSchema } from './schema';
|
||||
import { MeshShaderCode } from '../shader-code';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
|
||||
@@ -27,9 +27,10 @@ export const TextureMeshSchema = {
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
uBumpAmplitude: UniformSpec('f', 'material'),
|
||||
uInteriorColor: UniformSpec('v4'),
|
||||
uInteriorSubstance: UniformSpec('v4'),
|
||||
meta: ValueSpec('unknown')
|
||||
meta: ValueSpec('unknown'),
|
||||
|
||||
...InteriorSchema,
|
||||
...AnimationSchema,
|
||||
};
|
||||
export type TextureMeshSchema = typeof TextureMeshSchema
|
||||
export type TextureMeshValues = Values<TextureMeshSchema>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
@@ -63,6 +63,7 @@ interface Renderer {
|
||||
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => void
|
||||
clearDepth: (packed?: boolean) => void
|
||||
update: (camera: ICamera, scene: Scene) => void
|
||||
setTime: (time: number) => void
|
||||
|
||||
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', pickType: PickType) => void
|
||||
renderDepth: (group: Scene.Group, camera: ICamera) => void
|
||||
@@ -121,6 +122,8 @@ export const RendererParams = {
|
||||
}] }),
|
||||
ambientColor: PD.Color(Color.fromNormalizedRgb(1.0, 1.0, 1.0)),
|
||||
ambientIntensity: PD.Numeric(0.4, { min: 0.0, max: 2.0, step: 0.01 }),
|
||||
|
||||
enableAnimation: PD.Boolean(true, { description: 'Enable time-based animations.' }),
|
||||
};
|
||||
export type RendererProps = PD.Values<typeof RendererParams>
|
||||
|
||||
@@ -277,6 +280,9 @@ namespace Renderer {
|
||||
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
|
||||
uCelSteps: ValueCell.create(p.celSteps),
|
||||
uExposure: ValueCell.create(p.exposure),
|
||||
|
||||
uTime: ValueCell.create(0),
|
||||
uEnableAnimation: ValueCell.create(p.enableAnimation),
|
||||
};
|
||||
const globalUniformList = Object.entries(globalUniforms);
|
||||
|
||||
@@ -829,6 +835,9 @@ namespace Renderer {
|
||||
renderWboitTransparent,
|
||||
renderDpoitTransparent,
|
||||
|
||||
setTime: (time: number) => {
|
||||
ValueCell.updateIfChanged(globalUniforms.uTime, time);
|
||||
},
|
||||
setProps: (props: Partial<RendererProps>) => {
|
||||
if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
|
||||
p.backgroundColor = props.backgroundColor;
|
||||
@@ -904,6 +913,11 @@ namespace Renderer {
|
||||
Vec3.scale(ambientColor, Color.toArrayNormalized(p.ambientColor, ambientColor, 0), p.ambientIntensity);
|
||||
ValueCell.update(globalUniforms.uAmbientColor, ambientColor);
|
||||
}
|
||||
|
||||
if (props.enableAnimation !== undefined && props.enableAnimation !== p.enableAnimation) {
|
||||
p.enableAnimation = props.enableAnimation;
|
||||
ValueCell.update(globalUniforms.uEnableAnimation, p.enableAnimation);
|
||||
}
|
||||
},
|
||||
setViewport: (x: number, y: number, width: number, height: number) => {
|
||||
state.viewport(x, y, width, height);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -92,12 +92,16 @@ interface Scene extends Object3D {
|
||||
readonly markerAverage: number
|
||||
/** Emissive average of primitive renderables */
|
||||
readonly emissiveAverage: number
|
||||
/** Wiggle average of primitive renderables */
|
||||
readonly wiggleAverage: number
|
||||
/** Opacity average of primitive renderables */
|
||||
readonly opacityAverage: number
|
||||
/** Transparency minimum, excluding fully opaque, of primitive renderables */
|
||||
readonly transparencyMin: number
|
||||
/** Is `true` if any primitive renderable (possibly) has any opaque part */
|
||||
readonly hasOpaque: boolean
|
||||
/** Is `true` if any primitive renderable has animation enabled */
|
||||
readonly hasAnimation: boolean
|
||||
}
|
||||
|
||||
namespace Scene {
|
||||
@@ -119,15 +123,19 @@ namespace Scene {
|
||||
|
||||
let markerAverageDirty = true;
|
||||
let emissiveAverageDirty = true;
|
||||
let wiggleAverageDirty = true;
|
||||
let opacityAverageDirty = true;
|
||||
let transparencyMinDirty = true;
|
||||
let hasOpaqueDirty = true;
|
||||
let hasAnimationDirty = true;
|
||||
|
||||
let markerAverage = 0;
|
||||
let emissiveAverage = 0;
|
||||
let wiggleAverage = 0;
|
||||
let opacityAverage = 0;
|
||||
let transparencyMin = 0;
|
||||
let hasOpaque = false;
|
||||
let hasAnimation = false;
|
||||
|
||||
const object3d = Object3D.create();
|
||||
const { view, position, direction, up } = object3d;
|
||||
@@ -185,9 +193,11 @@ namespace Scene {
|
||||
renderables.sort(renderableSort);
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -211,9 +221,11 @@ namespace Scene {
|
||||
boundingSphereVisibleDirty = true;
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
visibleHash = newVisibleHash;
|
||||
return true;
|
||||
} else {
|
||||
@@ -245,6 +257,18 @@ namespace Scene {
|
||||
return count > 0 ? emissiveAverage / count : 0;
|
||||
}
|
||||
|
||||
function calculateWiggleAverage() {
|
||||
if (primitives.length === 0) return 0;
|
||||
let count = 0;
|
||||
let wiggleAverage = 0;
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
if (!primitives[i].state.visible) continue;
|
||||
wiggleAverage += primitives[i].values.wiggleAverage.ref.value;
|
||||
count += 1;
|
||||
}
|
||||
return count > 0 ? wiggleAverage / count : 0;
|
||||
}
|
||||
|
||||
function calculateOpacityAverage() {
|
||||
if (primitives.length === 0) return 0;
|
||||
let count = 0;
|
||||
@@ -301,6 +325,22 @@ namespace Scene {
|
||||
return false;
|
||||
}
|
||||
|
||||
function calculateHasAnimation() {
|
||||
for (let i = 0, il = primitives.length; i < il; ++i) {
|
||||
const p = primitives[i];
|
||||
if (!p.state.visible) continue;
|
||||
|
||||
if ((p.values.uWiggleAmplitude?.ref.value > 0 || p.values.wiggleAverage.ref.value > 0) &&
|
||||
p.values.uWiggleSpeed?.ref.value > 0 &&
|
||||
p.values.uWiggleFrequency?.ref.value > 0) return true;
|
||||
|
||||
if (p.values.uTumbleAmplitude?.ref.value > 0 &&
|
||||
p.values.uTumbleSpeed?.ref.value > 0 &&
|
||||
p.values.uTumbleFrequency?.ref.value > 0) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
view, position, direction, up,
|
||||
|
||||
@@ -341,9 +381,11 @@ namespace Scene {
|
||||
}
|
||||
markerAverageDirty = true;
|
||||
emissiveAverageDirty = true;
|
||||
wiggleAverageDirty = true;
|
||||
opacityAverageDirty = true;
|
||||
transparencyMinDirty = true;
|
||||
hasOpaqueDirty = true;
|
||||
hasAnimationDirty = true;
|
||||
},
|
||||
add: (o: GraphicsRenderObject) => commitQueue.add(o),
|
||||
remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
|
||||
@@ -401,6 +443,13 @@ namespace Scene {
|
||||
}
|
||||
return emissiveAverage;
|
||||
},
|
||||
get wiggleAverage() {
|
||||
if (wiggleAverageDirty) {
|
||||
wiggleAverage = calculateWiggleAverage();
|
||||
wiggleAverageDirty = false;
|
||||
}
|
||||
return wiggleAverage;
|
||||
},
|
||||
get opacityAverage() {
|
||||
if (opacityAverageDirty) {
|
||||
opacityAverage = calculateOpacityAverage();
|
||||
@@ -422,6 +471,13 @@ namespace Scene {
|
||||
}
|
||||
return hasOpaque;
|
||||
},
|
||||
get hasAnimation() {
|
||||
if (hasAnimationDirty) {
|
||||
hasAnimation = calculateHasAnimation();
|
||||
hasAnimationDirty = false;
|
||||
}
|
||||
return hasAnimation;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ import { clip_instance } from './shader/chunks/clip-instance.glsl';
|
||||
import { clip_pixel } from './shader/chunks/clip-pixel.glsl';
|
||||
import { color_frag_params } from './shader/chunks/color-frag-params.glsl';
|
||||
import { color_vert_params } from './shader/chunks/color-vert-params.glsl';
|
||||
import { common_animation } from './shader/chunks/common-animation.glsl';
|
||||
import { common_clip } from './shader/chunks/common-clip.glsl';
|
||||
import { common_frag_params } from './shader/chunks/common-frag-params.glsl';
|
||||
import { common_vert_params } from './shader/chunks/common-vert-params.glsl';
|
||||
@@ -97,6 +98,7 @@ const ShaderChunks: { [k: string]: string } = {
|
||||
clip_pixel,
|
||||
color_frag_params,
|
||||
color_vert_params,
|
||||
common_animation,
|
||||
common_clip,
|
||||
common_frag_params,
|
||||
common_vert_params,
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
export const assign_position = `
|
||||
mat4 model = uModel * aTransform;
|
||||
#ifdef dGeometryType_image
|
||||
mat4 transform = aTransform;
|
||||
#else
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
#endif
|
||||
mat4 model = uModel * transform;
|
||||
mat4 modelView = uView * model;
|
||||
#ifdef dGeometryType_textureMesh
|
||||
vec3 position = readFromTexture(tPosition, vertexId, uGeoTexDim).xyz;
|
||||
#else
|
||||
vec3 position = aPosition;
|
||||
#endif
|
||||
#ifndef dGeometryType_image
|
||||
position = applyWiggle(position, group, aInstance);
|
||||
#endif
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
// for accessing tColorGrid in vert shader and for clipping in frag shader
|
||||
vModelPosition = (model * position4).xyz;
|
||||
|
||||
102
src/mol-gl/shader/chunks/common-animation.glsl.ts
Normal file
102
src/mol-gl/shader/chunks/common-animation.glsl.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const common_animation = `
|
||||
uniform float uWiggleSpeed;
|
||||
uniform float uWiggleAmplitude;
|
||||
uniform float uWiggleFrequency;
|
||||
uniform int uWiggleMode;
|
||||
uniform float uTumbleSpeed;
|
||||
uniform float uTumbleAmplitude;
|
||||
uniform float uTumbleFrequency;
|
||||
|
||||
#ifdef dWiggle
|
||||
uniform vec2 uWiggleTexDim;
|
||||
uniform sampler2D tWiggle;
|
||||
uniform float uWiggleStrength;
|
||||
#endif
|
||||
|
||||
vec3 applyWiggle(vec3 pos, float groupId, float instanceId) {
|
||||
if (!uEnableAnimation) return pos;
|
||||
float amplitude = uWiggleAmplitude;
|
||||
#ifdef dWiggle
|
||||
#if defined(dWiggleType_instance)
|
||||
amplitude += readFromTexture(tWiggle, instanceId, uWiggleTexDim).a * uWiggleStrength;
|
||||
#elif defined(dWiggleType_groupInstance)
|
||||
amplitude += readFromTexture(tWiggle, instanceId * float(uGroupCount) + groupId, uWiggleTexDim).a * uWiggleStrength;
|
||||
#endif
|
||||
#endif
|
||||
if (amplitude > 0.0 && uWiggleSpeed > 0.0 && uWiggleFrequency > 0.0) {
|
||||
float t = uTime * uWiggleSpeed;
|
||||
vec3 s;
|
||||
if (uWiggleMode == 0) {
|
||||
// Position mode: spatial position correlates nearby atoms
|
||||
s = pos;
|
||||
} else {
|
||||
// Group mode: per-group independent noise
|
||||
// Hash groupId into a well-distributed 3D seed to avoid repetition
|
||||
s = vec3(
|
||||
fract(sin(groupId * 127.1) * 43758.5453) * 1000.0,
|
||||
fract(sin(groupId * 269.5) * 21639.7182) * 1000.0,
|
||||
fract(sin(groupId * 419.2) * 32517.3926) * 1000.0
|
||||
);
|
||||
}
|
||||
s *= uWiggleFrequency;
|
||||
pos.x += (fbm(vec3(s.x, s.y + t, s.z)) / 0.4375 - 1.0) * amplitude;
|
||||
pos.y += (fbm(vec3(s.x + 37.0, s.y, s.z + t)) / 0.4375 - 1.0) * amplitude;
|
||||
pos.z += (fbm(vec3(s.x + t, s.y + 73.0, s.z)) / 0.4375 - 1.0) * amplitude;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
mat4 applyTumble(mat4 transform, float instanceIndex, float objectId) {
|
||||
if (!uEnableAnimation) return transform;
|
||||
if (uTumbleAmplitude > 0.0 && uTumbleSpeed > 0.0 && uTumbleFrequency > 0.0) {
|
||||
// Scale amplitude inversely with bounding-sphere radius (Stokes-Einstein: D ~ 1/r)
|
||||
float amplitude = uTumbleAmplitude / max(uInvariantBoundingSphere.w, 1.0);
|
||||
float t = uTime * uTumbleSpeed;
|
||||
float seed = (instanceIndex * 127.1 + objectId * 311.7) * uTumbleFrequency;
|
||||
|
||||
// Per-instance rotation angles from layered noise (Brownian-like)
|
||||
float angleX = (fbm(vec3(seed, t, 0.0)) / 0.4375 - 1.0) * amplitude;
|
||||
float angleY = (fbm(vec3(seed, 0.0, t)) / 0.4375 - 1.0) * amplitude;
|
||||
float angleZ = (fbm(vec3(0.0, seed, t)) / 0.4375 - 1.0) * amplitude;
|
||||
|
||||
float cx = cos(angleX); float sx = sin(angleX);
|
||||
float cy = cos(angleY); float sy = sin(angleY);
|
||||
float cz = cos(angleZ); float sz = sin(angleZ);
|
||||
|
||||
// Combined rotation matrix (Rz * Ry * Rx)
|
||||
mat3 rot = mat3(
|
||||
cy * cz, cx * sz + sx * sy * cz, sx * sz - cx * sy * cz,
|
||||
-cy * sz, cx * cz - sx * sy * sz, sx * cz + cx * sy * sz,
|
||||
sy, -sx * cy, cx * cy
|
||||
);
|
||||
|
||||
// Per-instance translation offset from layered noise (Brownian-like)
|
||||
vec3 offset = vec3(
|
||||
(fbm(vec3(seed + 31.7, t, 0.0)) / 0.4375 - 1.0),
|
||||
(fbm(vec3(seed + 31.7, 0.0, t)) / 0.4375 - 1.0),
|
||||
(fbm(vec3(0.0, seed + 31.7, t)) / 0.4375 - 1.0)
|
||||
) * amplitude;
|
||||
|
||||
// Bounding-sphere center transformed by the linear part only (no translation)
|
||||
vec3 localCenter = mat3(transform) * uInvariantBoundingSphere.xyz;
|
||||
|
||||
// Rotate basis vectors
|
||||
mat4 result = transform;
|
||||
result[0].xyz = rot * transform[0].xyz;
|
||||
result[1].xyz = rot * transform[1].xyz;
|
||||
result[2].xyz = rot * transform[2].xyz;
|
||||
|
||||
// Adjust translation so rotation pivots around the transformed center
|
||||
result[3].xyz = transform[3].xyz + localCenter - rot * localCenter + offset;
|
||||
|
||||
return result;
|
||||
}
|
||||
return transform;
|
||||
}
|
||||
`;
|
||||
@@ -121,34 +121,6 @@ vec3 perturbNormal(in vec3 position, in vec3 normal, in float height, in float s
|
||||
return normalize(abs(det) * normal - scale * surfGrad);
|
||||
}
|
||||
|
||||
float hash(in float h) {
|
||||
return fract(sin(h) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(in vec3 x) {
|
||||
vec3 p = floor(x);
|
||||
vec3 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float n = p.x + p.y * 157.0 + 113.0 * p.z;
|
||||
return mix(
|
||||
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),
|
||||
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
|
||||
mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
float fbm(in vec3 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5 * noise(p);
|
||||
p *= 2.01;
|
||||
f += 0.25 * noise(p);
|
||||
p *= 2.02;
|
||||
f += 0.125 * noise(p);
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
#ifdef dXrayShaded
|
||||
float calcXrayShadedAlpha(in float alpha, const in vec3 normal) {
|
||||
#if defined(dXrayShaded_on)
|
||||
|
||||
@@ -12,6 +12,8 @@ uniform vec4 uLod;
|
||||
|
||||
uniform bool uDoubleSided;
|
||||
uniform int uPickType;
|
||||
uniform float uTime;
|
||||
uniform bool uEnableAnimation;
|
||||
|
||||
#if dClipObjectCount != 0
|
||||
uniform int uClipObjectType[dClipObjectCount];
|
||||
|
||||
@@ -279,4 +279,32 @@ mat3 adjoint(const in mat4 m) {
|
||||
#define isNaN isnan
|
||||
#define isInf isinf
|
||||
#endif
|
||||
|
||||
float hash(in float h) {
|
||||
return fract(sin(h) * 43758.5453123);
|
||||
}
|
||||
|
||||
float noise(in vec3 x) {
|
||||
vec3 p = floor(x);
|
||||
vec3 f = fract(x);
|
||||
f = f * f * (3.0 - 2.0 * f);
|
||||
|
||||
float n = p.x + p.y * 157.0 + 113.0 * p.z;
|
||||
return mix(
|
||||
mix(mix(hash(n + 0.0), hash(n + 1.0), f.x),
|
||||
mix(hash(n + 157.0), hash(n + 158.0), f.x), f.y),
|
||||
mix(mix(hash(n + 113.0), hash(n + 114.0), f.x),
|
||||
mix(hash(n + 270.0), hash(n + 271.0), f.x), f.y), f.z);
|
||||
}
|
||||
|
||||
float fbm(in vec3 p) {
|
||||
float f = 0.0;
|
||||
f += 0.5 * noise(p);
|
||||
p *= 2.01;
|
||||
f += 0.25 * noise(p);
|
||||
p *= 2.02;
|
||||
f += 0.125 * noise(p);
|
||||
|
||||
return f;
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform mat4 uModelView;
|
||||
|
||||
@@ -46,11 +47,14 @@ void main() {
|
||||
#include assign_clipping_varying
|
||||
#include assign_size
|
||||
|
||||
mat4 modelTransform = uModel * aTransform;
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
vec3 wigStart = applyWiggle(aStart, aGroup, aInstance);
|
||||
vec3 wigEnd = applyWiggle(aEnd, aGroup, aInstance);
|
||||
mat4 modelTransform = uModel * transform;
|
||||
|
||||
vTransform = modelTransform;
|
||||
vStart = (modelTransform * vec4(aStart, 1.0)).xyz;
|
||||
vEnd = (modelTransform * vec4(aEnd, 1.0)).xyz;
|
||||
vStart = (modelTransform * vec4(wigStart, 1.0)).xyz;
|
||||
vEnd = (modelTransform * vec4(wigEnd, 1.0)).xyz;
|
||||
vSize = size * aScale * uModelScale;
|
||||
vCap = aCap;
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ uniform sampler2D tMarker;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying float vInstance;
|
||||
varying vec3 vPosition;
|
||||
|
||||
#ifdef dUsePalette
|
||||
uniform sampler2D tPalette;
|
||||
@@ -138,7 +139,7 @@ uniform float uIsoLevel;
|
||||
#endif
|
||||
|
||||
void main() {
|
||||
if (uTrimType != 0 && getSignedDistance(vModelPosition, uTrimType, uTrimCenter, uTrimRotation, uTrimScale, uTrimTransform) > 0.0) discard;
|
||||
if (uTrimType != 0 && getSignedDistance(vPosition, uTrimType, uTrimCenter, uTrimRotation, uTrimScale, uTrimTransform) > 0.0) discard;
|
||||
|
||||
#include fade_lod
|
||||
#include clip_pixel
|
||||
|
||||
@@ -17,6 +17,7 @@ attribute float aInstance;
|
||||
|
||||
varying vec2 vUv;
|
||||
varying float vInstance;
|
||||
varying vec3 vPosition;
|
||||
|
||||
void main() {
|
||||
int vertexId = VertexID;
|
||||
@@ -25,5 +26,6 @@ void main() {
|
||||
|
||||
vUv = aUv;
|
||||
vInstance = aInstance;
|
||||
vPosition = aPosition;
|
||||
}
|
||||
`;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*
|
||||
@@ -16,6 +16,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform float uPixelRatio;
|
||||
uniform vec4 uViewport;
|
||||
@@ -48,18 +49,20 @@ void main(){
|
||||
#include assign_clipping_varying
|
||||
#include assign_size
|
||||
|
||||
mat4 modelView = uView * uModel * aTransform;
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
vec3 wigStart = applyWiggle(aStart, group, aInstance);
|
||||
vec3 wigEnd = applyWiggle(aEnd, group, aInstance);
|
||||
mat4 modelView = uView * uModel * transform;
|
||||
|
||||
// camera space
|
||||
vec4 start = modelView * vec4(aStart, 1.0);
|
||||
vec4 end = modelView * vec4(aEnd, 1.0);
|
||||
vec4 start = modelView * vec4(wigStart, 1.0);
|
||||
vec4 end = modelView * vec4(wigEnd, 1.0);
|
||||
|
||||
// assign position
|
||||
vec4 position4 = vec4((aMapping.y < 0.5) ? aStart : aEnd, 1.0);
|
||||
vec4 mvPosition = modelView * position4;
|
||||
vViewPosition = mvPosition.xyz;
|
||||
vec4 position4 = vec4((aMapping.y < 0.5) ? wigStart : wigEnd, 1.0);
|
||||
vViewPosition = (aMapping.y < 0.5) ? start.xyz : end.xyz;
|
||||
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
vModelPosition = (uModel * transform * position4).xyz; // for clipping in frag shader
|
||||
|
||||
// special case for perspective projection, and segments that terminate either in, or behind, the camera plane
|
||||
// clearly the gpu firmware has a way of addressing this issue when projecting into ndc space
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp sampler2D;
|
||||
#include common_vert_params
|
||||
#include color_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
#include texture3d_from_2d_linear
|
||||
|
||||
#ifdef dGeometryType_textureMesh
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform float uPixelRatio;
|
||||
uniform vec4 uViewport;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -14,6 +14,7 @@ precision highp int;
|
||||
#include color_vert_params
|
||||
#include size_vert_params
|
||||
#include common_clip
|
||||
#include common_animation
|
||||
|
||||
uniform mat4 uModelView;
|
||||
uniform mat4 uInvProjection;
|
||||
@@ -68,7 +69,7 @@ const mat4 D = mat4(
|
||||
* "GPU-Based Ray-Casting of Quadratic Surfaces" http://dl.acm.org/citation.cfm?id=2386396
|
||||
* by Christian Sigg, Tim Weyrich, Mario Botsch, Markus Gross.
|
||||
*/
|
||||
void quadraticProjection(const in vec3 position, const in float radius, const in vec2 mapping) {
|
||||
void quadraticProjection(const in vec3 position, const in float radius, const in vec2 mapping, const in mat4 transform) {
|
||||
vec2 xbc, ybc;
|
||||
|
||||
mat4 T = mat4(
|
||||
@@ -78,7 +79,7 @@ void quadraticProjection(const in vec3 position, const in float radius, const in
|
||||
position.x, position.y, position.z, 1.0
|
||||
);
|
||||
|
||||
mat4 R = transpose4(uProjection * uModelView * aTransform * T);
|
||||
mat4 R = transpose4(uProjection * uModelView * transform * T);
|
||||
float A = dot(R[3], D * R[3]);
|
||||
float B = -2.0 * dot(R[0], D * R[3]);
|
||||
float C = dot(R[0], D * R[0]);
|
||||
@@ -119,6 +120,9 @@ void main(void){
|
||||
vec3 position = positionGroup.rgb;
|
||||
float group = positionGroup.a;
|
||||
|
||||
position = applyWiggle(position, group, aInstance);
|
||||
mat4 transform = applyTumble(aTransform, aInstance, float(uObjectId));
|
||||
|
||||
#include assign_color_varying
|
||||
#include assign_marker_varying
|
||||
#include assign_clipping_varying
|
||||
@@ -127,7 +131,7 @@ void main(void){
|
||||
vRadius = size * uModelScale;
|
||||
|
||||
vec4 position4 = vec4(position, 1.0);
|
||||
vModelPosition = (uModel * aTransform * position4).xyz; // for clipping in frag shader
|
||||
vModelPosition = (uModel * transform * position4).xyz; // for clipping in frag shader
|
||||
|
||||
float d;
|
||||
if (uLod.w != 0.0 && (uLod.x != 0.0 || uLod.y != 0.0)) {
|
||||
@@ -143,7 +147,7 @@ void main(void){
|
||||
}
|
||||
}
|
||||
|
||||
vec4 mvPosition = uModelView * aTransform * position4;
|
||||
vec4 mvPosition = uModelView * transform * position4;
|
||||
|
||||
#ifdef dApproximate
|
||||
vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
|
||||
@@ -156,7 +160,7 @@ void main(void){
|
||||
gl_Position = uProjection * mvCorner;
|
||||
} else if (uIsAsymmetricProjection) {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
quadraticProjection(position, vRadius / uModelScale, mapping);
|
||||
quadraticProjection(position, vRadius / uModelScale, mapping, transform);
|
||||
} else {
|
||||
gl_Position = uProjection * vec4(mvPosition.xyz, 1.0);
|
||||
sphereProjection(mvPosition.xyz, vRadius, mapping);
|
||||
|
||||
@@ -104,7 +104,7 @@ export interface COMPAT_vertex_array_object {
|
||||
bindVertexArray(arrayObject: WebGLVertexArrayObject | null): void;
|
||||
createVertexArray(): WebGLVertexArrayObject | null;
|
||||
deleteVertexArray(arrayObject: WebGLVertexArrayObject): void;
|
||||
isVertexArray(value: any): value is WebGLVertexArrayObject;
|
||||
isVertexArray(value: any): boolean
|
||||
}
|
||||
|
||||
export function getVertexArrayObject(gl: GLRenderingContext): COMPAT_vertex_array_object | null {
|
||||
@@ -484,7 +484,7 @@ export interface COMPAT_disjoint_timer_query {
|
||||
/** Records the current time into the corresponding query object. */
|
||||
queryCounter: (query: WebGLQuery, target: number) => void
|
||||
/** Returns information about a query target. */
|
||||
getQuery: (target: number, pname: number) => WebGLQuery | number
|
||||
getQuery: (target: number, pname: number) => WebGLQuery | null
|
||||
/** Return the state of a query object. */
|
||||
getQueryParameter: (query: WebGLQuery, pname: number) => number | boolean
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ export function uint8ToString(array: Uint8Array) {
|
||||
if (array.length > ChunkSize) {
|
||||
const c = [];
|
||||
for (let i = 0; i < array.length; i += ChunkSize) {
|
||||
// @ts-ignore
|
||||
c.push(String.fromCharCode.apply(null, array.subarray(i, i + ChunkSize)));
|
||||
}
|
||||
return c.join('');
|
||||
} else {
|
||||
// @ts-ignore
|
||||
return String.fromCharCode.apply(null, array);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2026 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ export async function readCcp4Header(file: FileHandle): Promise<{ header: Ccp4He
|
||||
// 53 MAP Character string 'MAP ' to identify file type
|
||||
const MAP = String.fromCharCode(
|
||||
buffer.readUInt8(52 * 4), buffer.readUInt8(52 * 4 + 1),
|
||||
buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3)
|
||||
buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3) || 32 // null as space
|
||||
);
|
||||
if (MAP !== 'MAP ') {
|
||||
throw new Error('ccp4 format error, missing "MAP " string');
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.410, IHM 1.28, MA 1.4.9.
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.414, IHM 1.28, MA 1.4.9.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.410, IHM 1.28, MA 1.4.9.
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.414, IHM 1.28, MA 1.4.9.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user