mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 15:14:22 +08:00
Compare commits
371 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e843c20cc | ||
|
|
ecaf19c5fb | ||
|
|
f024aeef2c | ||
|
|
9d9985f117 | ||
|
|
a0f7349ef6 | ||
|
|
01407427d2 | ||
|
|
3dee03d9b6 | ||
|
|
737f6593be | ||
|
|
068e10dd40 | ||
|
|
c1ba5248b0 | ||
|
|
4af0f22ac0 | ||
|
|
25a67e1176 | ||
|
|
a8fcd501d6 | ||
|
|
573ee92889 | ||
|
|
2558d6fada | ||
|
|
2cf3f8d62b | ||
|
|
589d89b0d5 | ||
|
|
7cc7b77460 | ||
|
|
e8a9995bef | ||
|
|
74ff283e00 | ||
|
|
1ecb960b82 | ||
|
|
387d59f97b | ||
|
|
d993082f24 | ||
|
|
5eaa73d56d | ||
|
|
b9428fd3cd | ||
|
|
97d180b79d | ||
|
|
25bd915ea5 | ||
|
|
f8fdffdc44 | ||
|
|
d11aa6ea77 | ||
|
|
fc3c7997ea | ||
|
|
b3aecf8de4 | ||
|
|
f3581e62ef | ||
|
|
88e7fe508f | ||
|
|
98049ed02d | ||
|
|
194092ed67 | ||
|
|
e96157c890 | ||
|
|
a028c1ef42 | ||
|
|
ad2b5e687d | ||
|
|
8ba19f0be4 | ||
|
|
bccc68f6df | ||
|
|
026a05d03d | ||
|
|
2b4741c8ee | ||
|
|
7960ee06d4 | ||
|
|
f73f5af131 | ||
|
|
3123110aa4 | ||
|
|
154063638d | ||
|
|
a720b98365 | ||
|
|
d4a2937e0b | ||
|
|
b0ca7ffbb7 | ||
|
|
c42b738abe | ||
|
|
ab0d0fec53 | ||
|
|
8d96131962 | ||
|
|
95bbcd8b24 | ||
|
|
a21f5c2c23 | ||
|
|
94b7b1281c | ||
|
|
16dba586df | ||
|
|
72b761f959 | ||
|
|
943d81cbf9 | ||
|
|
2ecdc0eafa | ||
|
|
dccfd35c7a | ||
|
|
9e81a4f7a6 | ||
|
|
6f6cc73ce9 | ||
|
|
c248ae11bf | ||
|
|
742be03901 | ||
|
|
00009ef198 | ||
|
|
1cb617524d | ||
|
|
e2e348240b | ||
|
|
b54908492c | ||
|
|
33172862bd | ||
|
|
c5f2767efc | ||
|
|
66f5a81a5d | ||
|
|
9e90e11bfc | ||
|
|
ab372a89d6 | ||
|
|
ea612c3acb | ||
|
|
a1308645e5 | ||
|
|
c6506d515f | ||
|
|
794b705184 | ||
|
|
66264abe50 | ||
|
|
7d0f84ff72 | ||
|
|
31495ab02a | ||
|
|
853ad5c916 | ||
|
|
51fc525215 | ||
|
|
92d1c446d4 | ||
|
|
f2a0ff448b | ||
|
|
0ec096a980 | ||
|
|
44a5b83c1c | ||
|
|
46c5184d40 | ||
|
|
7c46306929 | ||
|
|
d7fe32d000 | ||
|
|
d7beb288c3 | ||
|
|
fb5da1b4d0 | ||
|
|
d89e254555 | ||
|
|
99e11317e1 | ||
|
|
3dc6c4452d | ||
|
|
3a627a878b | ||
|
|
6f9fed180d | ||
|
|
5ecd176f20 | ||
|
|
dff3837df6 | ||
|
|
e42eb31b73 | ||
|
|
721c117309 | ||
|
|
216715b2d5 | ||
|
|
412d4d5bcd | ||
|
|
2734d5754a | ||
|
|
c10f9d8c78 | ||
|
|
7140135cbe | ||
|
|
b5969945b4 | ||
|
|
7f5b3bc16c | ||
|
|
5cf7b6624b | ||
|
|
56225b337d | ||
|
|
79b6ad6f48 | ||
|
|
d0df53dd02 | ||
|
|
3b97bfd9b6 | ||
|
|
9b12623131 | ||
|
|
425370d63e | ||
|
|
1666c89222 | ||
|
|
a7dd4fc555 | ||
|
|
9f1760fbf2 | ||
|
|
d7fb040b77 | ||
|
|
2d7c1bcea2 | ||
|
|
a08c434f35 | ||
|
|
45d402bb9f | ||
|
|
4556544043 | ||
|
|
921d700761 | ||
|
|
9605783f41 | ||
|
|
f23329dc68 | ||
|
|
5f4ac6b2c0 | ||
|
|
f0c2961e95 | ||
|
|
2bdaa565b4 | ||
|
|
ab2bcde794 | ||
|
|
0b9674e14c | ||
|
|
07cbeb524e | ||
|
|
8ff75ea2ab | ||
|
|
6f5db94b2f | ||
|
|
2637957141 | ||
|
|
c1bb6f3987 | ||
|
|
d8df904951 | ||
|
|
a7ca7c922d | ||
|
|
f257992a5a | ||
|
|
62f9f6077d | ||
|
|
e4edb67f62 | ||
|
|
185ccf5ca6 | ||
|
|
bdd1805620 | ||
|
|
29f2722851 | ||
|
|
b38f8b08da | ||
|
|
6d02889f84 | ||
|
|
b864634f1d | ||
|
|
248662b95c | ||
|
|
0eb28bd89e | ||
|
|
e466bf9ba9 | ||
|
|
a14c4faefd | ||
|
|
b87a7f069e | ||
|
|
674a56e2f3 | ||
|
|
521d8cb4f8 | ||
|
|
bd1d85e927 | ||
|
|
4d62b928f8 | ||
|
|
014c9607d9 | ||
|
|
98ef24fc9e | ||
|
|
c04580377b | ||
|
|
a492b38368 | ||
|
|
518f21531e | ||
|
|
36fd40ee09 | ||
|
|
6b8c604762 | ||
|
|
c10382d1fb | ||
|
|
0e968ae59c | ||
|
|
1286a9e560 | ||
|
|
bf73712781 | ||
|
|
53922db113 | ||
|
|
799037d657 | ||
|
|
5cb7a3cc8e | ||
|
|
c14cbb258d | ||
|
|
8a860497f1 | ||
|
|
77d4d0007c | ||
|
|
005824eb24 | ||
|
|
259e04a6ce | ||
|
|
966bc14c67 | ||
|
|
f752b7e155 | ||
|
|
255b8b9ac3 | ||
|
|
15c4fb3c01 | ||
|
|
9fba0c08b2 | ||
|
|
f08dd0255d | ||
|
|
42d969bbeb | ||
|
|
fdc33e44dc | ||
|
|
b0aa889a0a | ||
|
|
4d7bd53231 | ||
|
|
c11cf665c9 | ||
|
|
a4b09d3a0c | ||
|
|
6e488b0f80 | ||
|
|
2cef723483 | ||
|
|
6164281a50 | ||
|
|
c74a014ab7 | ||
|
|
4bbf1dc8aa | ||
|
|
6e53621e01 | ||
|
|
2db7171e2a | ||
|
|
edfc094952 | ||
|
|
b3e1e2900b | ||
|
|
ba2bc206cc | ||
|
|
1e498d535a | ||
|
|
6ed969cd1b | ||
|
|
27bb4f4bca | ||
|
|
6ce2139272 | ||
|
|
856eff5127 | ||
|
|
13cf6613a6 | ||
|
|
52b141c4fa | ||
|
|
701844ca7c | ||
|
|
bcc572bd18 | ||
|
|
c5bb13e295 | ||
|
|
34c8257848 | ||
|
|
fcbf39c935 | ||
|
|
46c8150b2b | ||
|
|
af1a864daa | ||
|
|
3babd9399a | ||
|
|
e57564486f | ||
|
|
464a91ac29 | ||
|
|
4b58ce94ee | ||
|
|
16b0374eac | ||
|
|
67e63dccb4 | ||
|
|
2cc600cc52 | ||
|
|
27fa50a5de | ||
|
|
1e323f18f7 | ||
|
|
2685b2b77d | ||
|
|
d71b47a515 | ||
|
|
88cc720dd2 | ||
|
|
201433cc91 | ||
|
|
8582303491 | ||
|
|
655c3edadd | ||
|
|
a4323a4bd8 | ||
|
|
1b5a7d9546 | ||
|
|
f165cc4629 | ||
|
|
cb499ce42e | ||
|
|
db247d6fbd | ||
|
|
23701bf8e8 | ||
|
|
2e1f2e7eec | ||
|
|
fdb3ff54f1 | ||
|
|
d5fd56718d | ||
|
|
0698ac6dd5 | ||
|
|
825b59ab1e | ||
|
|
3086d1a5c8 | ||
|
|
138796862b | ||
|
|
1b236f1ae5 | ||
|
|
b6c2e25395 | ||
|
|
b7816986aa | ||
|
|
437c70a75a | ||
|
|
de85e0fbae | ||
|
|
8f7fda4919 | ||
|
|
470ccd333f | ||
|
|
2b6d067b0e | ||
|
|
0b928888a5 | ||
|
|
28edfd44cb | ||
|
|
3391c6de07 | ||
|
|
12b7951700 | ||
|
|
c527b59782 | ||
|
|
3bbbac66c7 | ||
|
|
c0980bf18a | ||
|
|
45eab19493 | ||
|
|
1e2a5a5bfd | ||
|
|
45edfa8014 | ||
|
|
899203c855 | ||
|
|
ef823b066b | ||
|
|
33dc2015df | ||
|
|
fcf5ea420b | ||
|
|
8d97327f8d | ||
|
|
cbc0e857fc | ||
|
|
01ce306405 | ||
|
|
a39a49e884 | ||
|
|
887a39dde9 | ||
|
|
abc7ebba3e | ||
|
|
73d593907e | ||
|
|
84a45fabdc | ||
|
|
0dc05e1138 | ||
|
|
dd11cacae4 | ||
|
|
ea17902aa6 | ||
|
|
b503259758 | ||
|
|
1e98741e16 | ||
|
|
f879519700 | ||
|
|
c6e175e5da | ||
|
|
add75bf9c9 | ||
|
|
57cbcd5fbf | ||
|
|
50a820b0ae | ||
|
|
0a33936e06 | ||
|
|
2abbb843f8 | ||
|
|
32179f31c2 | ||
|
|
7291025e09 | ||
|
|
0cb2c3621b | ||
|
|
86da258280 | ||
|
|
477a80d1ca | ||
|
|
86b68018a9 | ||
|
|
da095d6ef9 | ||
|
|
dc304b9e08 | ||
|
|
c905fa17c4 | ||
|
|
a06c64e8e0 | ||
|
|
f5441290dd | ||
|
|
9f23124317 | ||
|
|
8299cd638c | ||
|
|
50cb08e74d | ||
|
|
89552652ba | ||
|
|
37ce577813 | ||
|
|
4d9a003141 | ||
|
|
6f0311a53f | ||
|
|
bfd2d6b055 | ||
|
|
3072e60709 | ||
|
|
62ed8d10e3 | ||
|
|
13d3c34864 | ||
|
|
cac433efca | ||
|
|
b25ffe7151 | ||
|
|
31074dc74c | ||
|
|
c98c01a076 | ||
|
|
8966fc9396 | ||
|
|
fdbdc551e8 | ||
|
|
bb232ac3a4 | ||
|
|
735c25ef8d | ||
|
|
298043313a | ||
|
|
77cd181b91 | ||
|
|
b5bee042e8 | ||
|
|
4faf17ddc7 | ||
|
|
28774b2277 | ||
|
|
6a7444f44e | ||
|
|
15bfa8416a | ||
|
|
e6895ec833 | ||
|
|
2099ad728a | ||
|
|
72ae3fae65 | ||
|
|
bb5ad78681 | ||
|
|
f10e88612f | ||
|
|
a2e582d4a9 | ||
|
|
572874f4ae | ||
|
|
b9c0347497 | ||
|
|
089148198f | ||
|
|
6fc04c3294 | ||
|
|
dc55577e22 | ||
|
|
f7ba7c0511 | ||
|
|
ed5374fab9 | ||
|
|
9a04b4f0df | ||
|
|
9350e539b6 | ||
|
|
c38377af46 | ||
|
|
9804febd95 | ||
|
|
7936dc1840 | ||
|
|
a033a8be36 | ||
|
|
4b84c6dcba | ||
|
|
309d792fdb | ||
|
|
c437254680 | ||
|
|
6fbf7c7a22 | ||
|
|
86a7520b90 | ||
|
|
cd10043447 | ||
|
|
146e95cb23 | ||
|
|
13b1e5d59c | ||
|
|
ae3efa53d6 | ||
|
|
2e67fbe870 | ||
|
|
56df6f82a7 | ||
|
|
fdd874b7a6 | ||
|
|
f142c3ef1b | ||
|
|
978b53e7d8 | ||
|
|
2f3197479d | ||
|
|
6536d0ab91 | ||
|
|
3bee224e7d | ||
|
|
3e63137977 | ||
|
|
38d6bc6c27 | ||
|
|
fafe22d56b | ||
|
|
a6a92bcf91 | ||
|
|
82c681f445 | ||
|
|
fbbd58b4db | ||
|
|
2dc13f082c | ||
|
|
ab5eb5993d | ||
|
|
2384003f5d | ||
|
|
3675c0afe0 | ||
|
|
d9bae488e9 | ||
|
|
e31e5321ba | ||
|
|
8c7f8b8a56 | ||
|
|
e4dfb5148c | ||
|
|
39e2591b60 | ||
|
|
f8a5237024 | ||
|
|
6c2d5b9da7 | ||
|
|
e128d85356 |
@@ -1,4 +0,0 @@
|
||||
node_modules/*
|
||||
build/*
|
||||
docs/site/*
|
||||
lib/*
|
||||
122
.eslintrc.json
122
.eslintrc.json
@@ -1,122 +0,0 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018,
|
||||
"sourceType": "module",
|
||||
"ecmaFeatures": {
|
||||
"impliedStrict": true
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"indent": "off",
|
||||
"arrow-parens": [
|
||||
"off",
|
||||
"as-needed"
|
||||
],
|
||||
"brace-style": "off",
|
||||
"comma-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"comma-dangle": "off",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"import/order": "off",
|
||||
"no-eval": "warn",
|
||||
"no-new-wrappers": "warn",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-var": "error",
|
||||
"spaced-comment": "error",
|
||||
"semi": "warn",
|
||||
"no-restricted-syntax": [
|
||||
"error",
|
||||
{
|
||||
"selector": "ExportDefaultDeclaration",
|
||||
"message": "Default exports are not allowed"
|
||||
}
|
||||
],
|
||||
"no-throw-literal": "error",
|
||||
"key-spacing": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": "error",
|
||||
"space-in-parens": "error",
|
||||
"computed-property-spacing": "error",
|
||||
"prefer-const": ["error", {
|
||||
"destructuring": "all",
|
||||
"ignoreReadBeforeAssign": false
|
||||
}],
|
||||
"space-before-function-paren": "off",
|
||||
"func-call-spacing": "off",
|
||||
"no-multi-spaces": "error",
|
||||
"block-spacing": "error",
|
||||
"keyword-spacing": "off",
|
||||
"space-before-blocks": "error",
|
||||
"semi-spacing": "error",
|
||||
"no-constant-binary-expression": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.ts", "**/*.tsx"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json", "tsconfig.commonjs.json"],
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/indent": [
|
||||
"error",
|
||||
4
|
||||
],
|
||||
"@typescript-eslint/member-delimiter-style": [
|
||||
"off",
|
||||
{
|
||||
"multiline": {
|
||||
"delimiter": "none",
|
||||
"requireLast": true
|
||||
},
|
||||
"singleline": {
|
||||
"delimiter": "semi",
|
||||
"requireLast": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/quotes": [
|
||||
"error",
|
||||
"single",
|
||||
{
|
||||
"avoidEscape": true,
|
||||
"allowTemplateLiterals": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/semi": [
|
||||
"off",
|
||||
null
|
||||
],
|
||||
"@typescript-eslint/type-annotation-spacing": "error",
|
||||
"@typescript-eslint/brace-style": [
|
||||
"error",
|
||||
"1tbs", { "allowSingleLine": true }
|
||||
],
|
||||
"@typescript-eslint/comma-spacing": "error",
|
||||
"@typescript-eslint/space-infix-ops": "error",
|
||||
"@typescript-eslint/space-before-function-paren": ["error", {
|
||||
"anonymous": "always",
|
||||
"named": "never",
|
||||
"asyncArrow": "always"
|
||||
}],
|
||||
"@typescript-eslint/func-call-spacing": ["error"],
|
||||
"@typescript-eslint/keyword-spacing": ["error"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
2
.github/workflows/node.yml
vendored
2
.github/workflows/node.yml
vendored
@@ -13,7 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18
|
||||
node-version: 20
|
||||
- run: npm ci
|
||||
- run: sudo apt-get install xvfb
|
||||
- name: Lint
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
build/
|
||||
deploy/
|
||||
lib/
|
||||
docs/site/
|
||||
|
||||
@@ -13,3 +14,6 @@ tsconfig.commonjs.tsbuildinfo
|
||||
|
||||
.DS_Store
|
||||
tmp/
|
||||
|
||||
dev.pem
|
||||
dev-key.pem
|
||||
|
||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -6,9 +6,4 @@
|
||||
"*.vert.ts": "glsl",
|
||||
"*.gql.ts": "graphql"
|
||||
},
|
||||
"eslint.options": {
|
||||
"overrideConfig": {
|
||||
"ignorePatterns": ["webpack.config.js", "scripts/*"],
|
||||
},
|
||||
}
|
||||
}
|
||||
191
CHANGELOG.md
191
CHANGELOG.md
@@ -5,6 +5,197 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v5.4.2] - 2025-12-07
|
||||
- Fix postprocessing issues with SSAO and outlines for large structures (#1387)
|
||||
- Reduce automatic quality on standalone HMD devices
|
||||
|
||||
## [v5.4.1] - 2025-11-16
|
||||
- Fix ugly camera clipping in snapshot transitions
|
||||
- Add viewport button to toggle illumination mode
|
||||
- Fix bounding sphere computation for 3D text
|
||||
- Structure bounding sphere includes atom VDW radii / coarse sphere radii
|
||||
- Relax camera limits to allow focusing any selection with >1 atom
|
||||
- MolViewSpec
|
||||
- Fix `appendSnapshots` when loading MVSX
|
||||
- Fix all-selector color not applying on substructure
|
||||
- Fix primitives in root not being transformed with reference structure
|
||||
- Color themes do not prefer smoothing (improves performance in animations)
|
||||
- Allow canvas background interpolation
|
||||
- Fix `direct-volume` not drawn in illumination mode
|
||||
- Fix default trackball animated spin speed
|
||||
- Use `PluginCommands` to set canvas3d props in camera behavior
|
||||
- Volume improvements
|
||||
- Add `Volume.periodicity`
|
||||
- Wrap isosurfaces for periodic volumes
|
||||
- Fix dimensions for slices
|
||||
- Add support for Input Method Editor (IME) to text params input
|
||||
- Update `guessCifVariant` to detect density files not generated by the VolumeServer
|
||||
|
||||
## [v5.3.0] - 2025-11-05
|
||||
- Update loading message in MVS Stories Viewer
|
||||
- Add `Canvas3D.setAttribs`
|
||||
- Fix `normalizeWheel` "spin" calculation fallback
|
||||
- MolViewSpec
|
||||
- Add support for "topology" formats (TOP, PRMTOP, PSF)
|
||||
- Add support for additional "coordiates" formats (NCTRAJ, DCD, TRR)
|
||||
- Fix coarse structure selection
|
||||
- Fix missing default param values in `primitives_from_uri`
|
||||
|
||||
## [v5.2.0] - 2025-10-31
|
||||
- Handle transparency updates on ImagePass
|
||||
- Fix CIF parser edge case when the last token is escaped
|
||||
- MolViewSpec
|
||||
- Fix tooltips persisting across snapshots
|
||||
- Fix CIF annotations with no selector columns being ignored
|
||||
- Fix trackpad lock when camera up parallel to direction
|
||||
- Add clipping support for primitives
|
||||
- Support near camera distance
|
||||
|
||||
## [v5.1.2] - 2025-10-25
|
||||
- Fix createColorScaleByType when offsets are available
|
||||
- Get bond orders from non-standard CONECT records in PDB files
|
||||
- Remove outdated `gl_FrontFacing` workaround for buggy drivers
|
||||
- Fix clip objects for direct-volume rendering
|
||||
- Support "magic window" style AR (via WebXR)
|
||||
- Fix `PluginState.getStateTransitionFrameIndex`
|
||||
- Update `GlycamSaccharideNames` and `Monosaccharides` in `carbohydrates/constants.ts`
|
||||
- Support custom ref resolvers in `State`
|
||||
- Add full-screen mode support to layout manager
|
||||
- Add `show-toggle-fullscreen` URL param option to Viewer app
|
||||
- MolViewSpec
|
||||
- Support accessing Mol* State nodes by MVS-provided ref
|
||||
- Add support for DX map format
|
||||
- Better support for coarse structures in MVS:
|
||||
- Support for MVS annotations on coarse structures (color_from_*, tooltip_from_*)
|
||||
- Support for MVS labels on coarse structures (label, label_from_*)
|
||||
- (Other things already worked on coarse structures before: tooltip, color,component, primitives, component_from_*, primitives_from_*)
|
||||
- Tidy up MVS builder:
|
||||
- Add `sphere` and `angle` methods
|
||||
- [Breaking] Rename builder method primitives_from_uri -> primitivesFromUri
|
||||
|
||||
## [v5.0.0] - 2025-09-28
|
||||
- [Breaking] Renamed some color schemes ('inferno' -> 'inferno-no-black', 'magma' -> 'magma-no-black', 'turbo' -> 'turbo-no-black', 'rainbow' -> 'simple-rainbow')
|
||||
- [Breaking] `Box3D.nearestIntersectionWithRay` -> `Ray3D.intersectBox3D`
|
||||
- [Breaking] `Plane3D.distanceToSpher3D` -> `distanceToSphere3D` (fix spelling)
|
||||
- [Breaking] fix typo `MarchinCubes` -> `MarchingCubes`
|
||||
- [Breaking] `PluginContext.initViewer/initContainer/mount` are now async and have been renamed to include `Async` postfix
|
||||
- [Breaking] Add `Volume.instances` support and a `VolumeInstances` transform to dynamically assign it
|
||||
- This change is breaking because all volume objects require the `instances` field now.
|
||||
- [Breaking] `Canvas3D.identify` now expects `Vec2` or `Ray3D`
|
||||
- [Breaking] `TrackballControlsParams.animate.spin.speed` now means "Number of rotations per second" instead of "radians per second"
|
||||
- [Breaking] `PluginStateSnapshotManager.play` now accepts an options object instead of a single boolean value
|
||||
- Update production build to use `esbuild`
|
||||
- Emit explicit paths in `import`s in `lib/`
|
||||
- Fix outlines on opaque elements using illumination mode
|
||||
- Change `Representation.Empty` to a lazy property to avoid issue with some bundlers
|
||||
- MolViewSpec extension:
|
||||
- Generic color schemes (`palette` parameter for color_from_* nodes)
|
||||
- Annotation field remapping (`field_remapping` parameter for color_from_* nodes)
|
||||
- `representation` node: support custom property `molstar_representation_params`
|
||||
- Add `backbone` and `line` representation types
|
||||
- `primitives` node: support custom property `molstar_mesh/label/line_params`
|
||||
- `canvas` node: support custom property `molstar_postprocessing` with the ability to customize outline, depth of field, bloom, shadow, occlusion (SSAO), fog, and background
|
||||
- `clip` node support for structure and volume representations
|
||||
- `grid_slice` representation support for volumes
|
||||
- Support tethers and background for primitive labels
|
||||
- Support `snapshot_key` parameter on primitives that enables transition between states via clicking on 3D objects
|
||||
- Inline selectors and MVS annotations support `instance_id`
|
||||
- Support `matrix` on transform params
|
||||
- Support `surface_type` (`molecular` / `gaussian`) on for `surface` representation nodes
|
||||
- Add `instance` node type
|
||||
- Add `transform.rotation_center` property that enables rotating an object around its centroid or a specific point
|
||||
- Support transforming and instancing of structures, components, and volumes
|
||||
- Use params hash for node version for more performant tree diffs
|
||||
- Add `Snapshot.animation` support that enables animating almost every property in a given tree
|
||||
- Add `createMVSX` helper function
|
||||
- Support Mol* trackball animation via `animation.custom.molstar_trackball`
|
||||
- MVSX - use Murmur hash instead of FNV in archive URI
|
||||
- Support additional file formats (pdbqt, gro, xyz, mol, sdf, mol2, xtc, lammpstrj)
|
||||
- Support loading trajectory coordinates from separate nodes
|
||||
- Trigger markdown commands from primitives using `molstar_markdown_commands` custom extensions
|
||||
- Support `molstar_on_load_markdown_commands` custom state on the `root` node
|
||||
- Print tree validation errors to plugin log
|
||||
- Added new color schemes, synchronized with D3.js ('inferno', 'magma', 'turbo', 'rainbow', 'sinebow', 'warm', 'cool', 'cubehelix-default', 'category-10', 'observable-10', 'tableau-10')
|
||||
- Snapshot Markdown improvements
|
||||
- Add `MarkdownExtensionManager` (`PluginContext.managers.markdownExtensions`)
|
||||
- Support custom markdown commands to control the plugin via the `[link](!command)` pattern
|
||||
- Support rendering custom elements via the `` pattern
|
||||
- Support tables
|
||||
- Support loading images and audio from MVSX files
|
||||
- Indicate external links with ⤴
|
||||
- Audio support
|
||||
- Add `PluginState.Snapshot.onLoadMarkdownCommands`
|
||||
- Avoid calculating rings for coarse-grained structures
|
||||
- Fix isosurface compute shader normals when transformation matrix is applied to volume
|
||||
- Symmetry operator naming for spacegroup symmetry - parenthesize multi-character indices (1_111-1 -> 1_(11)1(-1))
|
||||
- Add `SymmetryOperator.instanceId` that corresponds to a canonical operator name (e.g. ASM-1, ASM-X0-1 for assemblies, 1_555, 1_(11)1(-1) for crystals)
|
||||
- Mol2 Reader
|
||||
- Fix column count parsing
|
||||
- Add support for substructure
|
||||
- Fix shader error when clipping flags are set without clip objects present
|
||||
- Fix wrong group count calculation on geometry update (#1562)
|
||||
- Fix wrong instance index in `calcMeshColorSmoothing`
|
||||
- Add `Ray3D` object and helpers
|
||||
- Volume slice representation: add `relativeX/Y/Z` options for dimension
|
||||
- Add `StructureInstances` transform
|
||||
- `mvs-stories` app
|
||||
- Add `story-id` URL arg support
|
||||
- Add `story-session-url` URL arg support
|
||||
- Add "Download MVS State" link
|
||||
- Add "Open in Mol*" link
|
||||
- Add "Edit in MolViewStories" link for story states
|
||||
- Add ray-based picking
|
||||
- Render narrow view of scene scene from ray origin & direction to a few pixel sized viewport
|
||||
- Cast ray on every input as opposed to the standard "whole screen" picking
|
||||
- Can be enabled with new `Canvas3dInteractionHelperParams.convertCoordsToRay` param
|
||||
- Allows to have input methods that are 3D pointers in the scene
|
||||
- Add `ray: Ray3D` property to `DragInput`, `ClickInput`, and `MoveInput`
|
||||
- Add async, non-blocking picking (only WebGL2)
|
||||
- Refactor `Canvas3dInteractionHelper` internals to use async picking for move events
|
||||
- Add `enable` param for post-processing effects. If false, no effects are applied.
|
||||
- Dot volume representation improvements
|
||||
- Add positional perturbation to avoid camera artifacts
|
||||
- Fix handling of negative isoValues by considering only volume cells with values lower than isoValue (#1559)
|
||||
- Fix volume-value size theme
|
||||
- Change the parsing of residue names in PDB files from 3-letter to 4-letter.
|
||||
- Support versioning transform using a hash function in `mol-state`
|
||||
- Support for "state snapshot transitions"
|
||||
- Add `PluginState.Snapshot.transition` that enables associating a state snapshot with a list states that can be animated
|
||||
- Add `AnimateStateSnapshotTransition` animation
|
||||
- Update the snapshots UI to support this feature
|
||||
- Use "proper time" in the animation loop to prevent animation skips during blocking operations (e.g., shader complication)
|
||||
- Add `Hsl` and (normalized) `Rgb` color spaces
|
||||
- Add `Color.interpolateHsl`
|
||||
- Add `rotationCenter` property to `TransformParam`
|
||||
- Add Monolayer transparency (exploiting dpoit).
|
||||
- Add plugin config item ShowReset (shows/hides "Reset Zoom" button)
|
||||
- Fix transform params not being normalized when used together with param hash version
|
||||
- Replace `immer` with `mutative`
|
||||
- Fix renderer transparency check
|
||||
- Add outlines improvements
|
||||
- VolumeServer & "VolumeCIF": default to P 1 spacegroup
|
||||
- Fix `ColorScale` for continuous case without offsets (broke in v4.13.0)
|
||||
- Experimental: support for custom color themes in Sequence Panel
|
||||
- Switch files.rcsb.org validation report URL to new endpoint /validation/view
|
||||
- Improve picking of objects with too many groups, pick whole instance/object
|
||||
- Add WebXR support
|
||||
- Requires immersive AR/VR headset
|
||||
- Supplements non-XR: enter/exit XR anytime and see (mostly) the same scene
|
||||
- Add `Canvas3D.xr` for managing XR sessions
|
||||
- Add `PointerHelper` for rendering XR input devices
|
||||
- Add XR button to Viewer and Mesoscale Explorer
|
||||
- Add XR button to render-structure in tests/browser
|
||||
- Fix illumination denoising with transparency on transparent background
|
||||
- Change the `to_mmCIF` function parameter from `structure` to `structures` to support either a single structure or an array of structures
|
||||
- ModelServer and VolumeServer: add configurable robots.txt
|
||||
- Adaptive parallel shader compilation
|
||||
- Split shader compilation into linking and finalizing
|
||||
- Start linking as early as possible and wait with finalizing to avoid blocking main thread
|
||||
- Use of `KHR_parallel_shader_compile` extension when available to check status
|
||||
- Add `ShaderManager` to compile shaders based on `Canvas3D` params and `Scene` content
|
||||
- Draw `Scene` only when shaders are ready
|
||||
- Fix incorrect animation loop handling in the screenshot code
|
||||
|
||||
## [v4.18.0] - 2025-06-08
|
||||
- MolViewSpec extension:
|
||||
- Support for label_comp_id and auth_comp_id in annotations
|
||||
|
||||
@@ -190,9 +190,14 @@ To get syntax highlighting for shader files add the following to Visual Code's s
|
||||
npm publish
|
||||
|
||||
## Deploy
|
||||
To prepare apps and demos for https://molstar.org deploy, run:
|
||||
|
||||
npm run test
|
||||
npm run build
|
||||
node ./scripts/deploy.js # currently updates the viewer on molstar.org/viewer
|
||||
npm run deploy:local
|
||||
|
||||
To commit these changes remotely to the `molstar/molstar.github.io` repo:
|
||||
|
||||
npm run deploy:remote
|
||||
|
||||
## Contributing
|
||||
Just open an issue or make a pull request. All contributions are welcome.
|
||||
|
||||
@@ -24,7 +24,7 @@ npm install
|
||||
Afterwards, build the project source:
|
||||
|
||||
```
|
||||
npm run build-tsc
|
||||
npm run build:lib
|
||||
```
|
||||
|
||||
and run the server by
|
||||
|
||||
@@ -94,7 +94,7 @@ The extension uses several transformations to process and visualize tunnel data:
|
||||
To help users understand how to use these transformations in practice, include detailed examples:
|
||||
|
||||
### Visualizing Multiple Tunnels
|
||||
This example ([runVisualizeTunnels](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L19)) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
This example (see `src/extensions/sb-ncbr/tunnels/examples.ts#L19`) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
@@ -104,7 +104,7 @@ update.toRoot()
|
||||
```
|
||||
|
||||
### Visualizing a Single Tunnel
|
||||
This example ([runVisualizeTunnel](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L46)) shows how to visualize a single tunnel.
|
||||
This example (see `src/extensions/sb-ncbr/tunnels/examples.ts#L46`) shows how to visualize a single tunnel.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelFromRawData, {
|
||||
|
||||
@@ -247,7 +247,7 @@ async function init() {
|
||||
const canvas = <HTMLCanvasElement> document.getElementById('molstar-canvas');
|
||||
const parent = <HTMLDivElement> document.getElementById('molstar-parent');
|
||||
|
||||
if (!plugin.initViewer(canvas, parent)) {
|
||||
if (!(await plugin.initViewerAsync(canvas, parent))) {
|
||||
console.error('Failed to init Mol*');
|
||||
return;
|
||||
}
|
||||
|
||||
107
docs/docs/plugin/managers/markdown-extensions.md
Normal file
107
docs/docs/plugin/managers/markdown-extensions.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Markdown Extension Manager
|
||||
|
||||
The `markdownExtensions` manager in `PluginContext.manager` allows customizing
|
||||
the `Markdown` React component to enable executing commands and rendering custom content.
|
||||
|
||||
The main use case of this is enriching [MolViewSpec](`https://molstar.org/mol-view-spec`) support.
|
||||
|
||||
## API
|
||||
|
||||
- `PluginContext.manager.markdownExtensions.register*` functions can be used to register extensions and state/data resolvers to make the the manager work with plugin extension
|
||||
- `PluginContext.manager.markdownExtensions.remove*` can be used to dynamically remove the above
|
||||
|
||||
## Commands
|
||||
|
||||
Extends Markdown Hyperlink syntax to support expressions of the form `[title](!c1=v1&c2=v2&...)` into an executable command. The command can be executed either on click, mouse enter, or mouse leave.
|
||||
|
||||
Generally, the command should be URL encoded, e.g., `a b` => `a%20b` (in JS, `encodeURIComponent`, in Python `urllib.parse.quote_plus/urlencode`).
|
||||
|
||||
### Built-in Commands
|
||||
|
||||
- `center-camera` - Centers the camera
|
||||
- `apply-snapshot=key` - Loads snapshots with the provided key
|
||||
- `next-snapshot[=-1|1]` - Loads next/previous snapshot, the direction is optional and default to `1`
|
||||
- `play-snapshots` - Starts playback of state snapshots
|
||||
- `play-transition` - Plays an animation associated with the given snapshot
|
||||
- `stop-animation` - Stops currently playing animation
|
||||
- `focus-refs=ref1,ref2,...` - On click, focuses nodes with the provided refs
|
||||
- `highlight-refs=ref1,ref2,...` - On mouse over, highlights the provided refs
|
||||
- `query=...&lang=...&action=highlight,focus&focus-radius=...`
|
||||
- `query` is an expression (e.g., `resn HEM` when using PyMol syntax)
|
||||
- (optional) `lang` is one of `mol-script` (default), `pymol`, `vmd`, `jmol`
|
||||
- (optional) `action` is an array of `highlight` (default), `focus` (multiple actions can be specified)
|
||||
- (optional) `focus-radius` is extra distance applied when focusing the selection (default is `3`)
|
||||
- Example: `[HEM](!query=resn%20HEM%26lang=pymol&action=highlight,focus)` highlights or focuses the HEM residue (the query must be URL encoded because it contains spaces and possibly other special characters)
|
||||
- `play-audio=src`, `toggle-audio[=src]`, `stop-audio`, `pause-audio`, `dispose-audio` - Audio playback support
|
||||
|
||||
## Custom Content
|
||||
|
||||
Extends Markdown Image syntax to support expressions of the form `` to render custom elements instead.
|
||||
|
||||
### Built-in Custom Content
|
||||
- `color-swatch=color` - Renders a box with the provided color
|
||||
- Color palettes:
|
||||
- `color-palette-name=name` - Renders a gradient with the provided named color palette (see `mol-util/color/lists.ts` for supported color schemes)
|
||||
- `color-palette-colors=color1,color2` - Renders a gradient with the provided colors
|
||||
- `color-palette-width=CCS-value` - Specifies the width of the element, defaults to `150px`
|
||||
- `color-palette-height=CCS-value` - Specified the height of the element, defaults to `0.5em`
|
||||
- `color-palette-discrete` - Renders discrete color list instead of interpolating
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
```markdown
|
||||
### Highlight/Focus:
|
||||
-  [polymer](!highlight-refs=polymer&focus-refs=polymer)
|
||||
-  [ligand](!highlight-refs=ligand&focus-refs=ligand)
|
||||
- [both](!highlight-refs=polymer,ligand&focus-refs=polymer,ligand)
|
||||
|
||||
### Color Palettes
|
||||
|name|visual|
|
||||
|---:|---|
|
||||
|viridis||
|
||||
|rainbow (discrete)||
|
||||
|custom|)|
|
||||
|
||||
### Camera controls
|
||||
- [center](!center-camera)
|
||||
|
||||
### Image embedded in MVSX file
|
||||

|
||||
```
|
||||
|
||||
This works with the MolViewSpec state built by:
|
||||
|
||||
```py
|
||||
import molviewspec as mvs
|
||||
|
||||
builder = mvs.create_builder()
|
||||
|
||||
assets = {
|
||||
"1cbs.cif": "https://files.wwpdb.org/download/1cbs.cif",
|
||||
"logo.png": "https://molstar.org/img/molstar-logo.png",
|
||||
}
|
||||
|
||||
model = (
|
||||
builder.download(url="1cbs.cif")
|
||||
.parse(format="mmcif")
|
||||
.model_structure()
|
||||
)
|
||||
(
|
||||
model.component(selector="polymer")
|
||||
.representation(ref="polymer")
|
||||
.color(color="blue")
|
||||
)
|
||||
(
|
||||
model.component(selector="ligand")
|
||||
.representation(ref="ligand")
|
||||
.color(color="red")
|
||||
)
|
||||
|
||||
mvsx = mvs.MVSX(
|
||||
data=builder.get_state(
|
||||
description="""...""" # inline the code above
|
||||
),
|
||||
assets=assets
|
||||
)
|
||||
```
|
||||
@@ -25,7 +25,6 @@ markdown_extensions:
|
||||
generic: true
|
||||
# Scripts for rendering Latex equations (in addition to pymdownx.arithmatex):
|
||||
extra_javascript:
|
||||
- https://polyfill.io/v3/polyfill.min.js?features=es6
|
||||
- https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js
|
||||
nav:
|
||||
- 'index.md'
|
||||
@@ -38,6 +37,8 @@ nav:
|
||||
- Data State: 'plugin/data-state.md'
|
||||
- File Formats: 'plugin/file-formats.md'
|
||||
- CIF Schemas: 'plugin/cif-schemas.md'
|
||||
- Managers:
|
||||
- Markdown Extensions: 'plugin/managers/markdown-extensions.md'
|
||||
- State Transforms:
|
||||
- Custom Trajectory: 'plugin/transforms/custom-trajectory.md'
|
||||
- Custom Conformation: 'plugin/transforms/custom-conformation.md'
|
||||
@@ -59,5 +60,5 @@ nav:
|
||||
- Interactions: 'extensions/interactions.md'
|
||||
- Misc:
|
||||
- Interesting PDB entries: misc/interesting-pdb-entries.md
|
||||
- Exporting component data: exporting-components.md
|
||||
- Exporting component data: misc/exporting-components.md
|
||||
repo_url: https://github.com/molstar/docs
|
||||
|
||||
111
eslint.config.mjs
Normal file
111
eslint.config.mjs
Normal file
@@ -0,0 +1,111 @@
|
||||
import { defineConfig } from "eslint/config";
|
||||
import globals from "globals";
|
||||
import typescriptEslint from "@typescript-eslint/eslint-plugin";
|
||||
import tsParser from "@typescript-eslint/parser";
|
||||
|
||||
export default defineConfig([{
|
||||
ignores: [
|
||||
"node_modules/*",
|
||||
"build/*",
|
||||
"deploy/*",
|
||||
"docs/site/*",
|
||||
"lib/*",
|
||||
"eslint.config.mjs",
|
||||
"build.mjs",
|
||||
]
|
||||
},{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
|
||||
ecmaVersion: 2018,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
ecmaFeatures: {
|
||||
impliedStrict: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
indent: "off",
|
||||
"arrow-parens": ["off", "as-needed"],
|
||||
"brace-style": ["error", "1tbs", {
|
||||
allowSingleLine: true,
|
||||
}],
|
||||
"comma-spacing": "off",
|
||||
"space-infix-ops": "off",
|
||||
"comma-dangle": "off",
|
||||
quotes: ["warn", "single", { "allowTemplateLiterals": true, "avoidEscape": true }],
|
||||
eqeqeq: ["error", "smart"],
|
||||
"import/order": "off",
|
||||
"no-eval": "warn",
|
||||
"no-extend-native": "warn",
|
||||
"no-new-wrappers": "warn",
|
||||
"no-trailing-spaces": "error",
|
||||
"no-unsafe-finally": "warn",
|
||||
"no-self-compare": "warn",
|
||||
"no-var": "error",
|
||||
"spaced-comment": "error",
|
||||
semi: "warn",
|
||||
"no-restricted-syntax": ["error", {
|
||||
selector: "ExportDefaultDeclaration",
|
||||
message: "Default exports are not allowed",
|
||||
}],
|
||||
"no-throw-literal": "error",
|
||||
"key-spacing": "error",
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"array-bracket-spacing": "error",
|
||||
"space-in-parens": "error",
|
||||
"computed-property-spacing": "error",
|
||||
"prefer-const": ["error", {
|
||||
destructuring: "all",
|
||||
ignoreReadBeforeAssign: false,
|
||||
}],
|
||||
"space-before-function-paren": "off",
|
||||
"func-call-spacing": "off",
|
||||
"no-multi-spaces": "error",
|
||||
"block-spacing": "error",
|
||||
"keyword-spacing": "warn",
|
||||
"space-before-blocks": "error",
|
||||
"semi-spacing": "error",
|
||||
"no-constant-binary-expression": "error",
|
||||
},
|
||||
}, {
|
||||
files: ["**/*.ts", "**/*.tsx"],
|
||||
|
||||
plugins: {
|
||||
"@typescript-eslint": typescriptEslint,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 5,
|
||||
sourceType: "module",
|
||||
|
||||
parserOptions: {
|
||||
project: ["tsconfig.eslint.json"],
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/class-name-casing": "off",
|
||||
"@typescript-eslint/member-delimiter-style": ["off", {
|
||||
multiline: {
|
||||
delimiter: "none",
|
||||
requireLast: true,
|
||||
},
|
||||
|
||||
singleline: {
|
||||
delimiter: "semi",
|
||||
requireLast: false,
|
||||
},
|
||||
}],
|
||||
"@typescript-eslint/prefer-namespace-keyword": "warn",
|
||||
"@typescript-eslint/semi": ["off", null],
|
||||
},
|
||||
}]);
|
||||
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
BIN
examples/audio/AudioMOM1_A.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
BIN
examples/audio/AudioMOM1_B.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
BIN
examples/audio/AudioMOM1_C.mp3
Normal file
Binary file not shown.
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
BIN
examples/audio/AudioMOM1_D.mp3
Normal file
Binary file not shown.
BIN
examples/trajectory/protein.dcd
Normal file
BIN
examples/trajectory/protein.dcd
Normal file
Binary file not shown.
BIN
examples/trajectory/protein.nc
Normal file
BIN
examples/trajectory/protein.nc
Normal file
Binary file not shown.
264
examples/trajectory/protein.parm7
Normal file
264
examples/trajectory/protein.parm7
Normal file
@@ -0,0 +1,264 @@
|
||||
%VERSION VERSION_STAMP = V0001.000 DATE = 11/04/25 11:55:47
|
||||
%FLAG TITLE
|
||||
%FORMAT(20a4)
|
||||
alanine-dipeptide.solvated.pdb
|
||||
%FLAG POINTERS
|
||||
%FORMAT(10I8)
|
||||
22 7 12 9 25 11 39 19 0 0
|
||||
99 3 9 11 19 7 11 20 0 0
|
||||
0 0 0 0 0 0 0 1 10 0
|
||||
0 1
|
||||
%FLAG ATOM_NAME
|
||||
%FORMAT(20a4)
|
||||
H1 CH3 H2 H3 C O N H CA HA CB HB1 HB2 HB3 C O N H C H1
|
||||
H2 H3
|
||||
%FLAG ATOMIC_NUMBER
|
||||
%FORMAT(10I8)
|
||||
1 6 1 1 6 8 7 1 6 1
|
||||
6 1 1 1 6 8 7 1 6 1
|
||||
1 1
|
||||
%FLAG RESIDUE_LABEL
|
||||
%FORMAT(20a4)
|
||||
ACE ALA NME
|
||||
%FLAG RESIDUE_POINTER
|
||||
%FORMAT(10I8)
|
||||
1 7 17
|
||||
%FLAG RESIDUE_NUMBER
|
||||
%FORMAT(20I4)
|
||||
1 2 3
|
||||
%FLAG RESIDUE_ICODE
|
||||
%FORMAT(20a4)
|
||||
|
||||
%FLAG RESIDUE_CHAINID
|
||||
%FORMAT(20a4)
|
||||
B B B
|
||||
%FLAG SOLVENT_POINTERS
|
||||
%FORMAT(3I8)
|
||||
0 1 0
|
||||
%FLAG ATOMS_PER_MOLECULE
|
||||
%FORMAT(10I8)
|
||||
22
|
||||
%FLAG MASS
|
||||
%FORMAT(5E16.8)
|
||||
3.02400000E+00 5.96200000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 9.99400000E+00 3.02400000E+00
|
||||
5.96200000E+00 3.02400000E+00 3.02400000E+00 3.02400000E+00 1.20100000E+01
|
||||
1.60000000E+01 1.19940000E+01 3.02400000E+00 5.96200000E+00 3.02400000E+00
|
||||
3.02400000E+00 3.02400000E+00
|
||||
%FLAG CHARGE
|
||||
%FORMAT(5E16.8)
|
||||
2.04636429E+00 -6.67300626E+00 2.04636429E+00 2.04636429E+00 1.08823576E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 6.14091510E-01 1.49969529E+00
|
||||
-3.32556975E+00 1.09880469E+00 1.09880469E+00 1.09880469E+00 1.08841798E+01
|
||||
-1.03484442E+01 -7.57501011E+00 4.95464337E+00 -2.71512270E+00 1.77849648E+00
|
||||
1.77849648E+00 1.77849648E+00
|
||||
%FLAG AMBER_ATOM_TYPE
|
||||
%FORMAT(20a4)
|
||||
a0 a1 a0 a0 a2 a3 a4 a5 a1 a6 a1 a0 a0 a0 a2 a3 a4 a5 a1 a6
|
||||
a6 a6
|
||||
%FLAG ATOM_TYPE_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 1 1 3 4 5 6 2 7
|
||||
2 1 1 1 3 4 5 6 2 7
|
||||
7 7
|
||||
%FLAG NONBONDED_PARM_INDEX
|
||||
%FORMAT(10I8)
|
||||
1 2 4 7 11 16 22 2 3 5
|
||||
8 12 17 23 4 5 6 9 13 18
|
||||
24 7 8 9 10 14 19 25 11 12
|
||||
13 14 15 20 26 16 17 18 19 20
|
||||
21 27 22 23 24 25 26 27 28
|
||||
%FLAG LENNARD_JONES_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
7.51607703E+03 9.71708117E+04 1.04308023E+06 8.61541883E+04 9.24822269E+05
|
||||
8.19971662E+05 5.44261042E+04 6.47841732E+05 5.74393458E+05 3.79876399E+05
|
||||
8.96776989E+04 9.95480466E+05 8.82619071E+05 6.06829343E+05 9.44293233E+05
|
||||
1.07193645E+02 2.56678134E+03 2.27577560E+03 1.02595236E+03 2.12601181E+03
|
||||
1.39982777E-01 4.98586847E+03 6.78771368E+04 6.01816484E+04 3.69471530E+04
|
||||
6.20665998E+04 5.94667299E+01 3.25969625E+03
|
||||
%FLAG LENNARD_JONES_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
2.17257828E+01 1.26919150E+02 6.75612247E+02 1.12529845E+02 5.99015525E+02
|
||||
5.31102864E+02 1.11805549E+02 6.26720080E+02 5.55666449E+02 5.64885984E+02
|
||||
1.36131731E+02 7.36907417E+02 6.53361429E+02 6.77220874E+02 8.01323529E+02
|
||||
2.59456373E+00 2.06278363E+01 1.82891803E+01 1.53505284E+01 2.09604198E+01
|
||||
9.37598976E-02 1.76949863E+01 1.06076943E+02 9.40505981E+01 9.21192137E+01
|
||||
1.13252062E+02 1.93248820E+00 1.43076527E+01
|
||||
%FLAG NUMBER_EXCLUDED_ATOMS
|
||||
%FORMAT(10I8)
|
||||
6 7 4 3 7 3 10 4 10 7
|
||||
6 3 2 1 7 3 5 4 3 2
|
||||
1 1
|
||||
%FLAG EXCLUDED_ATOMS_LIST
|
||||
%FORMAT(10I8)
|
||||
2 3 4 5 6 7 3 4 5 6
|
||||
7 8 9 4 5 6 7 5 6 7
|
||||
6 7 8 9 10 11 15 7 8 9
|
||||
8 9 10 11 12 13 14 15 16 17
|
||||
9 10 11 15 10 11 12 13 14 15
|
||||
16 17 18 19 11 12 13 14 15 16
|
||||
17 12 13 14 15 16 17 13 14 15
|
||||
14 15 15 16 17 18 19 20 21 22
|
||||
17 18 19 18 19 20 21 22 19 20
|
||||
21 22 20 21 22 21 22 22 0
|
||||
%FLAG BOND_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.40000000E+02 4.34000000E+02 3.17000000E+02 5.70000000E+02 4.90000000E+02
|
||||
3.37000000E+02 3.10000000E+02
|
||||
%FLAG BOND_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.09000000E+00 1.01000000E+00 1.52200000E+00 1.22900000E+00 1.33500000E+00
|
||||
1.44900000E+00 1.52600000E+00
|
||||
%FLAG BONDS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 1 3 6 1 3 9 1 18
|
||||
21 2 24 27 1 30 33 1 30 36
|
||||
1 30 39 1 48 51 2 54 57 1
|
||||
54 60 1 54 63 1
|
||||
%FLAG BONDS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 3 12 15 4 12 18 5 18
|
||||
24 6 24 42 3 24 30 7 42 48
|
||||
5 42 45 4 48 54 6
|
||||
%FLAG ANGLE_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
3.50000000E+01 5.00000000E+01 5.00000000E+01 5.00000000E+01 8.00000000E+01
|
||||
7.00000000E+01 5.00000000E+01 8.00000000E+01 8.00000000E+01 6.30000000E+01
|
||||
6.30000000E+01
|
||||
%FLAG ANGLE_EQUIL_VALUE
|
||||
%FORMAT(5E16.8)
|
||||
1.91113553E+00 1.91113553E+00 2.09439510E+00 2.06018665E+00 2.10137642E+00
|
||||
2.03505391E+00 2.12755636E+00 2.14500965E+00 1.91462619E+00 1.92160751E+00
|
||||
1.93906080E+00
|
||||
%FLAG ANGLES_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 6 1 0 3 9 1 0 3
|
||||
12 2 6 3 9 1 6 3 12 2
|
||||
9 3 12 2 12 18 21 3 18 24
|
||||
27 2 21 18 24 4 24 30 33 2
|
||||
24 30 36 2 24 30 39 2 27 24
|
||||
30 2 27 24 42 2 33 30 36 1
|
||||
33 30 39 1 36 30 39 1 42 48
|
||||
51 3 48 54 57 2 48 54 60 2
|
||||
48 54 63 2 51 48 54 4 57 54
|
||||
60 1 57 54 63 1 60 54 63 1
|
||||
%FLAG ANGLES_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 15 5 3 12 18 6 12 18
|
||||
24 7 15 12 18 8 18 24 30 9
|
||||
18 24 42 10 24 42 45 5 24 42
|
||||
48 6 30 24 42 11 42 48 54 7
|
||||
45 42 48 8
|
||||
%FLAG DIHEDRAL_FORCE_CONSTANT
|
||||
%FORMAT(5E16.8)
|
||||
8.00000000E-01 8.00000000E-02 2.50000000E+00 2.50000000E+00 2.00000000E+00
|
||||
1.55555556E-01 1.10000000E+00 0.00000000E+00 0.00000000E+00 8.00000000E-01
|
||||
1.80000000E+00 4.20000000E-01 2.70000000E-01 5.50000000E-01 1.58000000E+00
|
||||
4.50000000E-01 4.00000000E-01 2.00000000E-01 2.00000000E-01 1.05000000E+01
|
||||
%FLAG DIHEDRAL_PERIODICITY
|
||||
%FORMAT(5E16.8)
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 2.00000000E+00 1.00000000E+00
|
||||
3.00000000E+00 2.00000000E+00 1.00000000E+00 1.00000000E+00 3.00000000E+00
|
||||
2.00000000E+00 3.00000000E+00 2.00000000E+00 3.00000000E+00 2.00000000E+00
|
||||
1.00000000E+00 3.00000000E+00 2.00000000E+00 1.00000000E+00 2.00000000E+00
|
||||
%FLAG DIHEDRAL_PHASE
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 3.14159265E+00 3.14159265E+00 3.14159265E+00 0.00000000E+00
|
||||
0.00000000E+00 3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00 3.14159265E+00
|
||||
3.14159265E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 3.14159265E+00
|
||||
%FLAG SCEE_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 0.00000000E+00
|
||||
1.20000000E+00 0.00000000E+00 1.20000000E+00 1.20000000E+00 1.20000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 1.20000000E+00 0.00000000E+00
|
||||
0.00000000E+00 1.20000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCNB_SCALE_FACTOR
|
||||
%FORMAT(5E16.8)
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
2.00000000E+00 0.00000000E+00 2.00000000E+00 2.00000000E+00 2.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 2.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 2.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
%FLAG DIHEDRALS_INC_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
0 3 12 15 1 0 3 -12 15 2
|
||||
3 12 18 21 3 6 3 12 15 1
|
||||
6 3 -12 15 2 9 3 12 15 1
|
||||
9 3 -12 15 2 15 12 18 21 4
|
||||
15 12 -18 21 5 18 24 30 33 6
|
||||
18 24 30 36 6 18 24 30 39 6
|
||||
24 42 48 51 3 27 24 30 33 6
|
||||
27 24 30 36 6 27 24 30 39 6
|
||||
27 24 42 45 1 27 24 -42 45 2
|
||||
42 24 30 33 6 42 24 30 36 6
|
||||
42 24 30 39 6 45 42 48 51 4
|
||||
45 42 -48 51 5 21 18 -24 -12 7
|
||||
51 48 -54 -42 7 51 48 54 60 8
|
||||
21 18 24 30 8 42 48 54 57 8
|
||||
6 3 12 18 9 42 48 54 63 8
|
||||
51 48 54 57 8 21 18 24 42 8
|
||||
0 3 12 18 9 42 48 54 60 8
|
||||
27 24 42 48 8 21 18 24 27 8
|
||||
51 48 54 63 8 9 3 12 18 9
|
||||
12 18 24 27 8
|
||||
%FLAG DIHEDRALS_WITHOUT_HYDROGEN
|
||||
%FORMAT(10I8)
|
||||
3 12 18 24 3 12 18 24 30 10
|
||||
12 18 -24 30 11 12 18 -24 30 5
|
||||
12 18 24 42 12 12 18 -24 42 13
|
||||
15 12 18 24 3 18 24 42 48 14
|
||||
18 24 -42 48 15 18 24 -42 48 16
|
||||
24 42 48 54 3 30 24 42 48 17
|
||||
30 24 -42 48 18 30 24 -42 48 19
|
||||
45 42 48 54 3 15 12 -18 -3 20
|
||||
45 42 -48 -24 20 18 24 42 45 8
|
||||
30 24 42 45 8
|
||||
%FLAG SOLTY
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_ACOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBOND_BCOEF
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG HBCUT
|
||||
%FORMAT(5E16.8)
|
||||
|
||||
%FLAG TREE_CHAIN_CLASSIFICATION
|
||||
%FORMAT(20a4)
|
||||
BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA BLA
|
||||
BLA BLA
|
||||
%FLAG JOIN_ARRAY
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG IROTAT
|
||||
%FORMAT(10I8)
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0 0 0 0 0 0 0 0 0
|
||||
0 0
|
||||
%FLAG BOX_DIMENSIONS
|
||||
%FORMAT(5E16.8)
|
||||
9.00000000E+01 3.00000000E+01 3.00000000E+01 3.00000000E+01
|
||||
%FLAG RADIUS_SET
|
||||
%FORMAT(1a80)
|
||||
0
|
||||
%FLAG RADII
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG SCREEN
|
||||
%FORMAT(5E16.8)
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00 0.00000000E+00
|
||||
0.00000000E+00 0.00000000E+00
|
||||
%FLAG IPOL
|
||||
%FORMAT(1I8)
|
||||
0
|
||||
26
examples/trajectory/protein.pdb
Normal file
26
examples/trajectory/protein.pdb
Normal file
@@ -0,0 +1,26 @@
|
||||
CRYST1 30.000 30.000 30.000 90.00 90.00 90.00 P 1 1
|
||||
ATOM 1 H1 ACE A 1 2.000 1.000 -0.000 0.00 0.00 H
|
||||
ATOM 2 CH3 ACE A 1 2.000 2.090 0.000 0.00 0.00 C
|
||||
ATOM 3 H2 ACE A 1 1.486 2.454 0.890 0.00 0.00 H
|
||||
ATOM 4 H3 ACE A 1 1.486 2.454 -0.890 0.00 0.00 H
|
||||
ATOM 5 C ACE A 1 3.427 2.641 -0.000 0.00 0.00 C
|
||||
ATOM 6 O ACE A 1 4.391 1.877 -0.000 0.00 0.00 O
|
||||
ATOM 7 N ALA A 2 3.555 3.970 -0.000 0.00 0.00 N
|
||||
ATOM 8 H ALA A 2 2.733 4.556 -0.000 0.00 0.00 H
|
||||
ATOM 9 CA ALA A 2 4.853 4.614 -0.000 0.00 0.00 C
|
||||
ATOM 10 HA ALA A 2 5.408 4.316 0.890 0.00 0.00 H
|
||||
ATOM 11 CB ALA A 2 5.661 4.221 -1.232 0.00 0.00 C
|
||||
ATOM 12 HB1 ALA A 2 5.123 4.521 -2.131 0.00 0.00 H
|
||||
ATOM 13 HB2 ALA A 2 6.630 4.719 -1.206 0.00 0.00 H
|
||||
ATOM 14 HB3 ALA A 2 5.809 3.141 -1.241 0.00 0.00 H
|
||||
ATOM 15 C ALA A 2 4.713 6.129 0.000 0.00 0.00 C
|
||||
ATOM 16 O ALA A 2 3.601 6.653 0.000 0.00 0.00 O
|
||||
ATOM 17 N NME A 3 5.846 6.835 0.000 0.00 0.00 N
|
||||
ATOM 18 H NME A 3 6.737 6.359 -0.000 0.00 0.00 H
|
||||
ATOM 19 C NME A 3 5.846 8.284 0.000 0.00 0.00 C
|
||||
ATOM 20 H1 NME A 3 4.819 8.648 0.000 0.00 0.00 H
|
||||
ATOM 21 H2 NME A 3 6.360 8.648 0.890 0.00 0.00 H
|
||||
ATOM 22 H3 NME A 3 6.360 8.648 -0.890 0.00 0.00 H
|
||||
TER 23 NME A 3
|
||||
CONECT 5 7
|
||||
CONECT 15 17
|
||||
14
examples/trajectory/protein.rst7
Normal file
14
examples/trajectory/protein.rst7
Normal file
@@ -0,0 +1,14 @@
|
||||
alanine-dipeptide.solvated.pdb
|
||||
22
|
||||
0.7494821 1.2436848 0.8743532 1.0856344 2.2423820 0.5955986
|
||||
0.4304414 2.9747953 1.0671825 1.0497815 2.3544810 -0.4880289
|
||||
2.5015950 2.4471725 1.0820421 3.1003812 1.5343071 1.6479120
|
||||
3.0220696 3.6519467 0.8741013 2.4411554 4.3533213 0.4373955
|
||||
4.3920715 4.0500473 1.2160543 4.7674596 3.4172266 2.0202454
|
||||
5.2805058 3.8202998 -0.0180103 4.9565949 4.4537317 -0.8438106
|
||||
6.3180425 4.0583459 0.2164072 5.2327259 2.7740601 -0.3200050
|
||||
4.4431625 5.5106563 1.7135265 3.4307644 6.2198007 1.6891606
|
||||
5.6170320 5.9613562 2.1744082 6.3997462 5.3231585 2.1616313
|
||||
5.8784762 7.3296314 2.6320299 5.1056278 8.0184146 2.2908769
|
||||
5.9253575 7.3544224 3.7207393 6.8360338 7.6745804 2.2419090
|
||||
30.0000000 30.0000000 30.0000000 90.0000000 90.0000000 90.0000000
|
||||
20713
package-lock.json
generated
20713
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
104
package.json
104
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.18.0",
|
||||
"version": "5.4.2",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -11,36 +11,29 @@
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"lint-fix": "eslint . --fix",
|
||||
"test": "npm install --no-save \"gl@^6.0.2\" && npm run lint && jest",
|
||||
"jest": "jest",
|
||||
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
|
||||
"clean": "node ./scripts/clean.js",
|
||||
"clean": "node ./scripts/clean.js --all",
|
||||
"clean:build": "node ./scripts/clean.js --build",
|
||||
"build": "npm run build:apps && npm run build:lib",
|
||||
"build:apps": "node ./scripts/build.mjs -a -e --prd",
|
||||
"build:lib": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\" && npm run build:lib-extra",
|
||||
"build:lib-extra": "node scripts/write-version.mjs && cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ && cpx \"src/**/*.{scss,html,ico,jpg}\" lib/commonjs/ && tsc-alias -p tsconfig.json",
|
||||
"rebuild": "npm run clean && npm run build",
|
||||
"build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
|
||||
"build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
|
||||
"build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/",
|
||||
"build-webpack": "webpack --mode production --config ./webpack.config.production.js",
|
||||
"build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
|
||||
"watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
|
||||
"watch-viewer": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer\"",
|
||||
"watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
|
||||
"watch-tsc": "tsc --watch --incremental",
|
||||
"watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
|
||||
"watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch",
|
||||
"watch-webpack": "webpack -w --mode development --stats minimal",
|
||||
"watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
|
||||
"watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
|
||||
"dev": "node build-dev.mjs",
|
||||
"dev:all": "node build-dev.mjs -a -e",
|
||||
"dev:viewer": "node build-dev.mjs -a viewer",
|
||||
"dev:apps": "node build-dev.mjs -a",
|
||||
"dev:examples": "node build-dev.mjs -e",
|
||||
"dev": "node ./scripts/build.mjs",
|
||||
"dev:all": "node ./scripts/build.mjs -a -e -bt",
|
||||
"dev:viewer": "node ./scripts/build.mjs -a viewer",
|
||||
"dev:apps": "node ./scripts/build.mjs -a",
|
||||
"dev:examples": "node ./scripts/build.mjs -e",
|
||||
"dev:browser-tests": "node ./scripts/build.mjs -bt",
|
||||
"serve": "http-server -p 1338 -g",
|
||||
"deploy:local": "npm run clean:build && npm run build:apps && node ./scripts/deploy.js --local",
|
||||
"deploy:remote": "npm run clean:build && npm run build:apps && node ./scripts/deploy.js",
|
||||
"model-server": "node lib/commonjs/servers/model/server.js",
|
||||
"model-server-watch": "nodemon --watch lib lib/commonjs/servers/model/server.js",
|
||||
"volume-server-test": "node lib/commonjs/servers/volume/server.js --idMap em 'test/${id}.mdb' --defaultPort 1336",
|
||||
@@ -81,7 +74,7 @@
|
||||
"js"
|
||||
],
|
||||
"transform": {
|
||||
"\\.ts$": "ts-jest"
|
||||
"\\.ts$": "esbuild-jest-transform"
|
||||
},
|
||||
"moduleDirectories": [
|
||||
"node_modules",
|
||||
@@ -127,67 +120,62 @@
|
||||
"Ventura Rivera <venturaxrivera@gmail.com>",
|
||||
"Andy Turner <agdturner@gmail.com>",
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>"
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>",
|
||||
"Kim Juho <juho_kim@outlook.com>",
|
||||
"Victoria Doshchenko <doshchenko.victoria@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react": "^18.3.26",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@types/webxr": "^0.5.24",
|
||||
"@typescript-eslint/eslint-plugin": "^8.48.1",
|
||||
"@typescript-eslint/parser": "^8.48.1",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.1",
|
||||
"cpx2": "^8.0.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"esbuild": "^0.25.5",
|
||||
"esbuild": "^0.27.1",
|
||||
"esbuild-jest-transform": "^2.0.1",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "^8.57.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"eslint": "^9.39.1",
|
||||
"fs-extra": "^11.3.2",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"jest": "^30.2.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"mini-css-extract-plugin": "^2.9.2",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.89.1",
|
||||
"sass-loader": "^16.0.5",
|
||||
"simple-git": "^3.28.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-jest": "^29.3.4",
|
||||
"typescript": "^5.8.3",
|
||||
"webpack": "^5.99.9",
|
||||
"webpack-cli": "^6.0.1"
|
||||
"sass": "^1.94.2",
|
||||
"simple-git": "^3.30.0",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.8.1",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^18.19.111",
|
||||
"@types/node-fetch": "^2.6.12",
|
||||
"@types/swagger-ui-dist": "3.30.5",
|
||||
"@types/express": "^5.0.6",
|
||||
"@types/node": "^20.19.25",
|
||||
"@types/node-fetch": "^2.6.13",
|
||||
"@types/swagger-ui-dist": "3.30.6",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.8.0",
|
||||
"compression": "^1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"express": "^5.2.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^5.1.2",
|
||||
"immutable": "^5.1.4",
|
||||
"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.24.0",
|
||||
"swagger-ui-dist": "^5.30.3",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.3"
|
||||
},
|
||||
|
||||
@@ -97,31 +97,52 @@ function examplesCssRenamePlugin({ root }) {
|
||||
};
|
||||
}
|
||||
|
||||
async function watch(app) {
|
||||
function resolveEntryPath(path) {
|
||||
if (!fs.existsSync(path)) {
|
||||
return path + 'x'; // fallback to .tsx
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function getPaths(app) {
|
||||
if (app.kind === 'app') {
|
||||
return {
|
||||
prefix: `./build/${app.name}`,
|
||||
entry: resolveEntryPath(`./src/apps/${app.name}/index.ts`),
|
||||
outfile: `./build/${app.name}/${app.filename || 'molstar.js'}`,
|
||||
};
|
||||
}
|
||||
if (app.kind === 'example') {
|
||||
return {
|
||||
prefix: `./build/examples/${app.name}`,
|
||||
entry: resolveEntryPath(`./src/examples/${app.name}/index.ts`),
|
||||
outfile: `./build/examples/${app.name}/${app.filename || 'index.js'}`,
|
||||
};
|
||||
}
|
||||
if (app.kind === 'browser-test') {
|
||||
return {
|
||||
prefix: `./build/tests/browser`,
|
||||
entry: resolveEntryPath(`./src/tests/browser/${app.name}.ts`),
|
||||
outfile: `./build/tests/browser/${app.name}.js`,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unknown app kind: ${app.kind}`);
|
||||
}
|
||||
|
||||
async function createBundle(app) {
|
||||
const { name, kind } = app;
|
||||
|
||||
const prefix = kind === 'app'
|
||||
? `./build/${name}`
|
||||
: `./build/examples/${name}`;
|
||||
|
||||
let entry = `./src/${kind}s/${name}/index.ts`;
|
||||
if (!fs.existsSync(entry)) {
|
||||
entry = `./src/${kind}s/${name}/index.tsx`;
|
||||
}
|
||||
|
||||
let filename = app.filename;
|
||||
if (!filename) {
|
||||
filename = kind === 'app' ? 'molstar.js' : 'index.js';
|
||||
}
|
||||
const { prefix, entry, outfile } = getPaths(app);
|
||||
const NODE_ENV_PRD = isProduction || process.env.NODE_ENV === 'production';
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [entry],
|
||||
tsconfig: './tsconfig.json',
|
||||
bundle: true,
|
||||
minify: isProduction,
|
||||
minifyIdentifiers: false,
|
||||
sourcemap: includeSourceMap,
|
||||
globalName: app.globalName || 'molstar',
|
||||
outfile: kind === 'app'
|
||||
? `./build/${name}/${filename}`
|
||||
: `./build/examples/${name}/${filename}`,
|
||||
outfile,
|
||||
plugins: [
|
||||
fileLoaderPlugin({ out: prefix }),
|
||||
sassPlugin({
|
||||
@@ -139,15 +160,41 @@ async function watch(app) {
|
||||
},
|
||||
color: true,
|
||||
logLevel: 'info',
|
||||
define: {
|
||||
'process.env.NODE_ENV': JSON.stringify(NODE_ENV_PRD ? 'production' : 'development'),
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
|
||||
__MOLSTAR_PLUGIN_VERSION__: JSON.stringify(VERSION),
|
||||
__MOLSTAR_BUILD_TIMESTAMP__: `${TIMESTAMP}`,
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
await ctx.watch();
|
||||
|
||||
if (!isProduction) await ctx.watch();
|
||||
}
|
||||
|
||||
function findBrowserTests(names) {
|
||||
const dir = path.resolve('./src', 'tests', 'browser');
|
||||
let files = fs.readdirSync(dir).filter(file => file.endsWith('.ts')).map(file => file.replace('.ts', ''));
|
||||
if (names.length) {
|
||||
files = files.filter(file => names.includes(file));
|
||||
}
|
||||
return files.map(name => ({ kind: 'browser-test', name }));
|
||||
}
|
||||
|
||||
const argParser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Mol* development build'
|
||||
description: 'Mol* Build'
|
||||
});
|
||||
argParser.add_argument('--prd', {
|
||||
help: 'Create a production build.',
|
||||
required: false,
|
||||
action: 'store_true',
|
||||
});
|
||||
argParser.add_argument('--no-src-map', {
|
||||
help: 'Do not include source map.',
|
||||
required: false,
|
||||
action: 'store_true',
|
||||
});
|
||||
argParser.add_argument('--apps', '-a', {
|
||||
help: 'Apps to build.',
|
||||
@@ -159,6 +206,11 @@ argParser.add_argument('--examples', '-e', {
|
||||
required: false,
|
||||
nargs: '*',
|
||||
});
|
||||
argParser.add_argument('--browser-tests', '-bt', {
|
||||
help: 'Browser Tests to build.',
|
||||
required: false,
|
||||
nargs: '*',
|
||||
});
|
||||
argParser.add_argument('--port', '-p', {
|
||||
help: 'Port.',
|
||||
required: false,
|
||||
@@ -174,11 +226,20 @@ argParser.add_argument('--host', {
|
||||
|
||||
const args = argParser.parse_args();
|
||||
|
||||
|
||||
const isProduction = !!args.prd;
|
||||
const includeSourceMap = !args.no_src_map;
|
||||
|
||||
const VERSION = isProduction ? JSON.parse(fs.readFileSync('./package.json', 'utf8')).version : '(dev build)';
|
||||
const TIMESTAMP = Date.now();
|
||||
|
||||
const apps = (!args.apps ? [] : (args.apps.length ? args.apps.map(a => findApp(a, 'app')).filter(a => a) : Apps.filter(a => a.kind === 'app')));
|
||||
const examples = (!args.examples ? [] : (args.examples.length ? args.examples.map(e => findApp(e, 'example')).filter(a => a) : Apps.filter(a => a.kind === 'example')));
|
||||
const browserTests = (!args.browser_tests ? [] : findBrowserTests(args.browser_tests));
|
||||
|
||||
console.log('Apps:', apps.map(a => a.name));
|
||||
console.log('Examples:', examples.map(e => e.name));
|
||||
console.log('Browser Tests', browserTests.map(e => e.name));
|
||||
console.log('');
|
||||
|
||||
function getLocalIPs() {
|
||||
@@ -198,27 +259,42 @@ function getLocalIPs() {
|
||||
|
||||
async function main() {
|
||||
const promises = [];
|
||||
for (const app of apps) promises.push(watch(app));
|
||||
for (const example of examples) promises.push(watch(example));
|
||||
console.log(isProduction ? 'Building apps...' : 'Initial build...');
|
||||
|
||||
console.log('Initial build...');
|
||||
for (const app of apps) promises.push(createBundle(app));
|
||||
for (const example of examples) promises.push(createBundle(example));
|
||||
for (const browserTest of browserTests) promises.push(createBundle(browserTest));
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log('Done.');
|
||||
|
||||
if (isProduction) {
|
||||
console.log('Done.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log('Initial build complete.');
|
||||
|
||||
const certfile = './dev.pem';
|
||||
const keyfile = './dev-key.pem';
|
||||
|
||||
const sslEnabled = fs.existsSync(certfile) && fs.existsSync(keyfile);
|
||||
const protocol = sslEnabled ? 'https' : 'http';
|
||||
|
||||
const ctx = await esbuild.context({});
|
||||
ctx.serve({
|
||||
servedir: './',
|
||||
port: args.port,
|
||||
host: '0.0.0.0', // Always listen on all interfaces
|
||||
certfile: sslEnabled ? certfile : undefined,
|
||||
keyfile: sslEnabled ? keyfile : undefined,
|
||||
});
|
||||
|
||||
console.log('');
|
||||
console.log(`Server URL: http://localhost:${args.port}`);
|
||||
console.log(`Server URL: ${protocol}://localhost:${args.port}`);
|
||||
if (args.host) {
|
||||
console.log('Available host addresses:');
|
||||
const ips = getLocalIPs();
|
||||
ips.forEach(ip => console.log(` http://${ip}:${args.port}`));
|
||||
ips.forEach(ip => console.log(` ${protocol}://${ip}:${args.port}`));
|
||||
}
|
||||
console.log('');
|
||||
console.log('Watching for changes...');
|
||||
@@ -226,4 +302,7 @@ async function main() {
|
||||
console.log('Press Ctrl+C to stop.');
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const argparse = require('argparse');
|
||||
|
||||
function removeDir(dirPath) {
|
||||
for (const ent of fs.readdirSync(dirPath)) {
|
||||
@@ -24,11 +25,29 @@ function remove(entryPath) {
|
||||
fs.unlinkSync(entryPath);
|
||||
}
|
||||
|
||||
const toClean = [
|
||||
path.resolve(__dirname, '../build'),
|
||||
path.resolve(__dirname, '../lib'),
|
||||
path.resolve(__dirname, '../tsconfig.tsbuildinfo'),
|
||||
];
|
||||
const argParser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Clean Script'
|
||||
});
|
||||
argParser.add_argument('--build', { required: false, action: 'store_true' });
|
||||
argParser.add_argument('--lib', { required: false, action: 'store_true' });
|
||||
argParser.add_argument('--all', { required: false, action: 'store_true' });
|
||||
const args = argParser.parse_args();
|
||||
|
||||
const toClean = [];
|
||||
|
||||
if (args.build || args.all) {
|
||||
toClean.push(path.resolve(__dirname, '../build'));
|
||||
toClean.push(path.resolve(__dirname, '../deploy/data'));
|
||||
}
|
||||
if (args.lib || args.all) {
|
||||
toClean.push(
|
||||
path.resolve(__dirname, '../lib'),
|
||||
path.resolve(__dirname, '../tsconfig.tsbuildinfo'),
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n###', 'cleaning', toClean.join(', '));
|
||||
|
||||
toClean.forEach(ph => {
|
||||
if (fs.existsSync(ph)) {
|
||||
|
||||
@@ -2,20 +2,24 @@
|
||||
* Copyright (c) 2019-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
const git = require('simple-git');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const fse = require("fs-extra");
|
||||
const fs = require('fs');
|
||||
const fse = require('fs-extra');
|
||||
const argparse = require('argparse');
|
||||
|
||||
const VERSION = require(path.resolve(__dirname, '../package.json')).version;
|
||||
const MVS_STORIES_VERSION = require(path.resolve(__dirname, '../src/apps/mvs-stories/version.ts')).VERSION;
|
||||
|
||||
const remoteUrl = "https://github.com/molstar/molstar.github.io.git";
|
||||
const remoteUrl = 'https://github.com/molstar/molstar.github.io.git';
|
||||
const dataDir = path.resolve(__dirname, '../data/');
|
||||
const buildDir = path.resolve(__dirname, '../build/');
|
||||
const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
const deployDir = path.resolve(__dirname, '../deploy/');
|
||||
const localPath = path.resolve(deployDir, 'data/');
|
||||
const repositoryPath = path.resolve(deployDir, 'molstar.github.io/');
|
||||
|
||||
const analyticsTag = /<!-- __MOLSTAR_ANALYTICS__ -->/g;
|
||||
const analyticsCode = `<!-- Cloudflare Web Analytics --><script defer src='https://static.cloudflareinsights.com/beacon.min.js' data-cf-beacon='{"token": "c414cbae2d284ea995171a81e4a3e721"}'></script><!-- End Cloudflare Web Analytics --><iframe src="https://web3dsurvey.com/collector-iframe.html" style="width: 1px; height: 1px;"></iframe>`;
|
||||
@@ -80,54 +84,106 @@ function copyMe() {
|
||||
addAnalytics(path.resolve(meDeployPath, 'index.html'));
|
||||
}
|
||||
|
||||
function copyMVSStories() {
|
||||
console.log('\n###', 'copy MVS stories files');
|
||||
const mvsStoriesBuildPath = path.resolve(buildDir, 'mvs-stories/');
|
||||
const mvsStoriesDeployPath = path.resolve(localPath, `stories-viewer/v${MVS_STORIES_VERSION}/`);
|
||||
fse.copySync(mvsStoriesBuildPath, mvsStoriesDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(mvsStoriesDeployPath, 'index.html'));
|
||||
|
||||
// TODO: add PWA
|
||||
// addManifest(path.resolve(mvsStoriesDeployPath, 'index.html'));
|
||||
// addPwa(path.resolve(mvsStoriesDeployPath, 'index.html'));
|
||||
}
|
||||
|
||||
function copyDemo(name) {
|
||||
console.log('\n###', `copy demo files for ${name}`);
|
||||
const demoBuildPath = path.resolve(buildDir, `examples/${name}/`);
|
||||
const demoDeployPath = path.resolve(localPath, `demos/${name}/`);
|
||||
fse.copySync(demoBuildPath, demoDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(demoDeployPath, 'index.html'));
|
||||
}
|
||||
|
||||
function copyDemos() {
|
||||
console.log('\n###', 'copy demos files');
|
||||
const lightingBuildPath = path.resolve(buildDir, 'examples/lighting/');
|
||||
const lightingDeployPath = path.resolve(localPath, 'demos/lighting/');
|
||||
fse.copySync(lightingBuildPath, lightingDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(lightingDeployPath, 'index.html'));
|
||||
|
||||
const orbitalsBuildPath = path.resolve(buildDir, 'examples/alpha-orbitals/');
|
||||
const orbitalsDeployPath = path.resolve(localPath, 'demos/alpha-orbitals/');
|
||||
fse.copySync(orbitalsBuildPath, orbitalsDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(orbitalsDeployPath, 'index.html'));
|
||||
copyDemo('lighting');
|
||||
copyDemo('alpha-orbitals');
|
||||
copyDemo('mvs-stories');
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
try {
|
||||
copyViewer();
|
||||
copyMe();
|
||||
copyMVSStories();
|
||||
copyDemos();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function copyToRepository() {
|
||||
console.log('\n###', 'copy repository files');
|
||||
fse.copySync(localPath, repositoryPath, { overwrite: true });
|
||||
}
|
||||
|
||||
function syncRepository() {
|
||||
console.log('\n###', 'sync repository');
|
||||
if (!fs.existsSync(path.resolve(repositoryPath, '.git/'))) {
|
||||
console.log('\n###', 'clone repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.clone(remoteUrl, repositoryPath)
|
||||
.fetch(['--all'])
|
||||
.exec(copyToRepository);
|
||||
} else {
|
||||
console.log('\n###', 'update repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.fetch(['--all'])
|
||||
.reset(['--hard', 'origin/master'])
|
||||
.exec(copyToRepository);
|
||||
}
|
||||
}
|
||||
|
||||
function commit() {
|
||||
console.log('\n###', 'commit changes');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.add(['-A'])
|
||||
.commit(`Updated Apps and Demos
|
||||
- Mol* version: ${VERSION}
|
||||
- MVS Stories version: ${MVS_STORIES_VERSION}`)
|
||||
.push();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
console.log('\n###', 'create localPath');
|
||||
fs.mkdirSync(localPath, { recursive: true });
|
||||
}
|
||||
|
||||
process.chdir(localPath);
|
||||
const argParser = new argparse.ArgumentParser({
|
||||
add_help: true,
|
||||
description: 'Mol* Deploy'
|
||||
});
|
||||
argParser.add_argument('--local',{
|
||||
help: 'Do not commit to remote repository.',
|
||||
required: false,
|
||||
action: 'store_true',
|
||||
});
|
||||
const args = argParser.parse_args();
|
||||
|
||||
if (!fs.existsSync(path.resolve(localPath, '.git/'))) {
|
||||
console.log('\n###', 'clone repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.clone(remoteUrl, localPath)
|
||||
.fetch(['--all'])
|
||||
.exec(copyFiles)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer & demos')
|
||||
.push();
|
||||
} else {
|
||||
console.log('\n###', 'update repository');
|
||||
git()
|
||||
.outputHandler(log)
|
||||
.fetch(['--all'])
|
||||
.reset(['--hard', 'origin/master'])
|
||||
.exec(copyFiles)
|
||||
.add(['-A'])
|
||||
.commit('updated viewer & demos')
|
||||
.push();
|
||||
}
|
||||
copyFiles();
|
||||
|
||||
if (args.local) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(repositoryPath)) {
|
||||
console.log('\n###', 'create repositoryPath');
|
||||
fs.mkdirSync(repositoryPath, { recursive: true });
|
||||
}
|
||||
|
||||
process.chdir(repositoryPath);
|
||||
syncRepository();
|
||||
commit();
|
||||
16
scripts/write-version.mjs
Normal file
16
scripts/write-version.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
|
||||
const VERSION = JSON.parse(fs.readFileSync('./package.json', 'utf8')).version;
|
||||
const TIMESTAMP = Date.now();
|
||||
const file = `export var PLUGIN_VERSION = '${VERSION}';\nexport var PLUGIN_VERSION_DATE = new Date(${TIMESTAMP})`;
|
||||
const files = ['./lib/mol-plugin/version.js', './lib/commonjs/mol-plugin/version.js'];
|
||||
for (const f of files) {
|
||||
if (!fs.existsSync(f)) continue;
|
||||
fs.writeFileSync(f, file);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -147,6 +147,7 @@ export class MesoscaleExplorer {
|
||||
behaviors: [
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraAxisHelper),
|
||||
PluginSpec.Behavior(PluginBehaviors.Camera.CameraControls),
|
||||
PluginSpec.Behavior(PluginBehaviors.State.SnapshotControls),
|
||||
|
||||
PluginSpec.Behavior(MesoFocusLoci),
|
||||
PluginSpec.Behavior(MesoSelectLoci),
|
||||
@@ -252,6 +253,10 @@ export class MesoscaleExplorer {
|
||||
},
|
||||
cameraFog: { name: 'off', params: {} },
|
||||
hiZ: { enabled: true },
|
||||
xr: {
|
||||
disablePostprocessing: false,
|
||||
sceneRadiusInMeters: 0.75,
|
||||
},
|
||||
});
|
||||
|
||||
plugin.representation.structure.registry.clear();
|
||||
@@ -261,7 +266,6 @@ export class MesoscaleExplorer {
|
||||
image: true,
|
||||
componentManager: false,
|
||||
structureSelection: true,
|
||||
behavior: true,
|
||||
});
|
||||
|
||||
plugin.managers.lociLabels.clearProviders();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -21,6 +21,7 @@ const Key = Binding.TriggerKey;
|
||||
const DefaultMesoFocusLociBindings = {
|
||||
clickCenter: Binding([
|
||||
Trigger(B.Flag.Primary, M.create()),
|
||||
Trigger(B.Flag.Trigger),
|
||||
], 'Camera center', 'Click element using ${triggers}'),
|
||||
clickCenterFocus: Binding([
|
||||
Trigger(B.Flag.Secondary, M.create()),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -24,7 +24,8 @@ const Trigger = Binding.Trigger;
|
||||
|
||||
const DefaultMesoSelectLociBindings = {
|
||||
click: Binding([
|
||||
Trigger(B.Flag.Primary, M.create())
|
||||
Trigger(B.Flag.Primary, M.create()),
|
||||
Trigger(B.Flag.Trigger),
|
||||
], 'Click', 'Click element using ${triggers}'),
|
||||
clickToggleSelect: Binding([
|
||||
Trigger(B.Flag.Primary, M.create({ shift: true })),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -18,12 +18,14 @@ import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
|
||||
import { XRManagerParams } from '../../../mol-canvas3d/helper/xr-manager';
|
||||
|
||||
const Spacer = () => <div style={{ height: '2em' }} />;
|
||||
|
||||
const ViewportParams = {
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
xr: PD.Group(XRManagerParams, { label: 'XR' }),
|
||||
};
|
||||
|
||||
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
|
||||
|
||||
@@ -9,13 +9,14 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import type { MVSStoriesViewerModel } from './elements/viewer';
|
||||
|
||||
export type MVSStoriesCommand =
|
||||
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array }
|
||||
| { kind: 'load-mvs', format?: 'mvsj' | 'mvsx', url?: string, data?: MVSData | string | Uint8Array<ArrayBuffer> }
|
||||
|
||||
|
||||
export class MVSStoriesContext {
|
||||
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
|
||||
state = {
|
||||
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
|
||||
currentStoryData: new BehaviorSubject<string | Uint8Array<ArrayBuffer> | undefined>(undefined),
|
||||
isLoading: new BehaviorSubject(false),
|
||||
};
|
||||
|
||||
@@ -27,7 +28,7 @@ export class MVSStoriesContext {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMVSStoriesContext(options?: { name?: string, container?: object }) {
|
||||
export function getMVSStoriesContext(options?: { name?: string, container?: object }): MVSStoriesContext {
|
||||
const container: any = options?.container ?? window;
|
||||
container.componentContexts ??= {};
|
||||
const name = options?.name ?? '<default>';
|
||||
|
||||
@@ -8,13 +8,12 @@ import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { getMVSStoriesContext, MVSStoriesContext } from '../context';
|
||||
import { MVSStoriesViewerModel } from './viewer';
|
||||
import Markdown from 'react-markdown';
|
||||
import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { MarkdownAnchor } from '../../../mol-plugin-ui/controls';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
import { CSSProperties } from 'react';
|
||||
import { CSSProperties, useEffect, useState } from 'react';
|
||||
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
readonly context: MVSStoriesContext;
|
||||
@@ -71,6 +70,28 @@ export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
}
|
||||
}
|
||||
|
||||
function Loading() {
|
||||
return <div>
|
||||
<div style={{ marginBottom: 16 }}><i>Loading times may vary depending on the story size, your internet connection, and device performance</i></div>
|
||||
<div>Fetching data<Dots /></div>
|
||||
<div>Generating animations<Dots /></div>
|
||||
<div>Preparing visuals<Dots /></div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function Dots() {
|
||||
const [dots, setDots] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
setDots(d => (d + 1) % 4);
|
||||
}, Math.random() * 500 + 300);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return <span>{'.'.repeat(dots)}</span>;
|
||||
}
|
||||
|
||||
export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnapshotMarkdownModel }) {
|
||||
const state = useBehavior(model.state);
|
||||
const isLoading = useBehavior(model.context.state.isLoading);
|
||||
@@ -80,7 +101,8 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={style} className={className}>
|
||||
<i>Loading...</i>
|
||||
<h3>The story will be ready momentarily</h3>
|
||||
<Loading />
|
||||
</div>;
|
||||
}
|
||||
|
||||
@@ -99,7 +121,7 @@ export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnaps
|
||||
<div style={{ flexGrow: 1, overflow: 'hidden', overflowY: 'auto', position: 'relative' }}>
|
||||
<div style={{ position: 'absolute', inset: 0 }}>
|
||||
<PluginReactContext.Provider value={model.viewer?.model.plugin as any}>
|
||||
<Markdown skipHtml components={{ a: MarkdownAnchor }}>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
<Markdown>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
</PluginReactContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
import { MolViewSpec } from '../../../extensions/mvs/behavior';
|
||||
import { loadMVSData } from '../../../extensions/mvs/components/formats';
|
||||
import { MVSData } from '../../../extensions/mvs/mvs-data';
|
||||
import { StringLike } from '../../../mol-io/common/string-like';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { createPluginUI } from '../../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../../mol-plugin-ui/react18';
|
||||
@@ -56,11 +58,17 @@ export class MVSStoriesViewerModel extends PluginComponent {
|
||||
try {
|
||||
this.context.state.isLoading.next(true);
|
||||
if (cmd.kind === 'load-mvs') {
|
||||
let loadedData: MVSData | StringLike | Uint8Array | undefined;
|
||||
if (cmd.url) {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url: cmd.url, type: cmd.format === 'mvsx' ? 'binary' : 'string' }));
|
||||
await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
|
||||
loadedData = await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
|
||||
} else if (cmd.data) {
|
||||
await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
|
||||
loadedData = await loadMVSData(this.plugin, cmd.data, cmd.format ?? 'mvsj');
|
||||
}
|
||||
if (StringLike.is(loadedData) || loadedData instanceof Uint8Array) {
|
||||
this.context.state.currentStoryData.next(loadedData as string | Uint8Array<ArrayBuffer>);
|
||||
} else if (loadedData) {
|
||||
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -38,6 +38,25 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
bottom: 4px;
|
||||
right: 8px;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 0.6rem;
|
||||
z-index: -1;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
#links a {
|
||||
color: #666;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#links .sep {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
@@ -68,15 +87,23 @@
|
||||
<body>
|
||||
<!-- the context-name parameter is optional and useful when embedding multiple stories in a single page -->
|
||||
<div id="viewer">
|
||||
<mvs-stories-viewer context-name="story1" />
|
||||
<mvs-stories-viewer context-name="story1" ></mvs-stories-viewer>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" />
|
||||
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" ></mvs-stories-snapshot-markdown>
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<span id="open-in-stories" style="display: none;"><a href="#" id="open-in-stories-link" target="_blank" rel="noopener noreferrer" title="Open and edit the story in the MolViewStories app">Edit in MolViewStories</a> <span class="sep">•</span></span>
|
||||
<span id="open-in-molstar" style="display: none;"><a href="#" id="open-in-molstar-link" target="_blank" rel="noopener noreferrer" title="Open the story in the Mol* Viewer app. Enables exporting an animation.">Open in Mol* Viewer</a> <span class="sep">•</span></span>
|
||||
<a href="#" id="mvs-data" title="MolViewSpec State for this story. Can be opened in the Mol* app.">Download MVS</a> <span class="sep">•</span> <a href="https://github.com/molstar/molstar/tree/master/src/apps/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
var storyId = urlParams.get('story-id');
|
||||
var storyUrl = urlParams.get('story-url');
|
||||
var storySessionUrl = urlParams.get('story-session-url');
|
||||
var format = urlParams.get('data-format');
|
||||
|
||||
// For testing purposes:
|
||||
@@ -84,11 +111,38 @@
|
||||
// storyUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/kinase-story.mvsj';
|
||||
// }
|
||||
|
||||
mvsStories.loadFromURL(
|
||||
storyUrl,
|
||||
{ format: format || 'mvsj', contextName: 'story1' },
|
||||
);
|
||||
var molstarDataLink = storyUrl;
|
||||
var editInStoriesUrl = undefined;
|
||||
|
||||
if (storyId) {
|
||||
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?published-session-id=' + storyId;
|
||||
molstarDataLink = 'https://stories.molstar.org/api/story/' + storyId + '/data';
|
||||
} else if (storyUrl) {
|
||||
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
|
||||
}
|
||||
|
||||
if (!editInStoriesUrl && storySessionUrl) {
|
||||
editInStoriesUrl = 'https://molstar.org/mol-view-stories/builder?session-url=' + encodeURIComponent(storySessionUrl);
|
||||
}
|
||||
|
||||
if (molstarDataLink) {
|
||||
var molstarLink = 'https://molstar.org/viewer?mvs-url=' + encodeURIComponent(molstarDataLink) + '&mvs-format=' + encodeURIComponent(format || 'mvsj');
|
||||
document.getElementById('open-in-molstar-link').setAttribute('href', molstarLink);
|
||||
document.getElementById('open-in-molstar').style.display = 'inline';
|
||||
}
|
||||
|
||||
if (editInStoriesUrl) {
|
||||
document.getElementById('open-in-stories-link').setAttribute('href', editInStoriesUrl);
|
||||
document.getElementById('open-in-stories').style.display = 'inline';
|
||||
}
|
||||
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
mvsStories.downloadCurrentStory({ contextName: 'story1' });
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -7,6 +7,7 @@
|
||||
import { getMVSStoriesContext } from './context';
|
||||
import './elements';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { download } from '../../mol-util/download';
|
||||
|
||||
import './favicon.ico';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
@@ -27,7 +28,7 @@ export function loadFromURL(url: string, options?: { format: 'mvsx' | 'mvsj', co
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function loadFromData(data: MVSData | string | Uint8Array, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
export function loadFromData(data: MVSData | string | Uint8Array<ArrayBuffer>, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
setTimeout(() => {
|
||||
getContext(options?.contextName).dispatch({
|
||||
kind: 'load-mvs',
|
||||
@@ -37,4 +38,27 @@ export function loadFromData(data: MVSData | string | Uint8Array, options?: { fo
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function getStoryUrlFromId(id: string, format: 'mvsx' | 'mvsj' = 'mvsj') {
|
||||
return `https://stories.molstar.org/api/story/${id}/data`;
|
||||
}
|
||||
|
||||
export function loadFromID(id: string, options?: { format?: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
loadFromURL(
|
||||
getStoryUrlFromId(id, options?.format),
|
||||
{ format: options?.format ?? 'mvsj', contextName: options?.contextName },
|
||||
);
|
||||
}
|
||||
|
||||
export function downloadCurrentStory(options?: { contextName?: string, filename?: string }) {
|
||||
const story = getContext(options?.contextName).state.currentStoryData.value;
|
||||
if (!story) return;
|
||||
|
||||
const isMVSJ = typeof story === 'string';
|
||||
const filename = `${options?.filename ?? 'story'}.${isMVSJ ? 'mvsj' : 'mvsx'}`;
|
||||
download(
|
||||
new Blob([typeof story === 'string' ? story : story.buffer], { type: isMVSJ ? 'application/json' : 'application/octet-stream' }),
|
||||
filename
|
||||
);
|
||||
};
|
||||
|
||||
export { MVSData };
|
||||
@@ -1,3 +1,5 @@
|
||||
@use '../../mol-plugin-ui/skin/base/components/markdown.scss';
|
||||
|
||||
.mvs-stories-markdown-explanation {
|
||||
// Adapted from skeleton.css, The MIT License (MIT), Copyright (c) 2011-2014 Dave Gamache
|
||||
line-height: 1.4;
|
||||
@@ -161,6 +163,34 @@
|
||||
border-width: 0;
|
||||
border-top: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid #E1E1E1;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid #E1E1E1;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d4ed7;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
|
||||
7
src/apps/mvs-stories/version.ts
Normal file
7
src/apps/mvs-stories/version.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const VERSION = 1;
|
||||
@@ -109,8 +109,11 @@ const DefaultViewerOptions = {
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowReset: PluginConfig.Viewport.ShowReset.defaultValue,
|
||||
viewportShowScreenshotControls: PluginConfig.Viewport.ShowScreenshotControls.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowToggleFullscreen: PluginConfig.Viewport.ShowToggleFullscreen.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: PluginConfig.Viewport.ShowSelectionMode.defaultValue,
|
||||
viewportShowAnimation: PluginConfig.Viewport.ShowAnimation.defaultValue,
|
||||
@@ -187,8 +190,11 @@ export class Viewer {
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.General.ResolutionMode, o.resolutionMode],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowReset, o.viewportShowReset],
|
||||
[PluginConfig.Viewport.ShowScreenshotControls, o.viewportShowScreenshotControls],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowToggleFullscreen, o.viewportShowToggleFullscreen],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, o.viewportShowSelectionMode],
|
||||
[PluginConfig.Viewport.ShowAnimation, o.viewportShowAnimation],
|
||||
@@ -525,7 +531,7 @@ export class Viewer {
|
||||
} else if (format === 'mvsx') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
|
||||
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data);
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
|
||||
}));
|
||||
} else {
|
||||
@@ -536,7 +542,7 @@ export class Viewer {
|
||||
/** Load MolViewSpec from `data`.
|
||||
* If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string.
|
||||
* If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */
|
||||
loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
loadMvsData(data: string | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
return loadMVSData(this.plugin, data, format, options);
|
||||
}
|
||||
|
||||
@@ -580,12 +586,12 @@ export interface VolumeIsovalueInfo {
|
||||
|
||||
export interface LoadTrajectoryParams {
|
||||
model: { kind: 'model-url', url: string, format?: BuiltInTrajectoryFormat /* mmcif */, isBinary?: boolean }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'model-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format?: BuiltInTrajectoryFormat /* mmcif */ }
|
||||
| { kind: 'topology-url', url: string, format: BuiltInTopologyFormat, isBinary?: boolean }
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInTopologyFormat },
|
||||
| { kind: 'topology-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInTopologyFormat },
|
||||
modelLabel?: string,
|
||||
coordinates: { kind: 'coordinates-url', url: string, format: BuiltInCoordinatesFormat, isBinary?: boolean }
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array, format: BuiltInCoordinatesFormat },
|
||||
| { kind: 'coordinates-data', data: string | number[] | ArrayBuffer | Uint8Array<ArrayBuffer>, format: BuiltInCoordinatesFormat },
|
||||
coordinatesLabel?: string,
|
||||
preset?: keyof PresetTrajectoryHierarchy
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 100px;
|
||||
top: 100px;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
@@ -66,6 +66,7 @@
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
var illumination = getParam('illumination', '[^&]+').trim() === '1';
|
||||
var resolutionMode = getParam('resolution-mode', '[^&]+').trim().toLowerCase();
|
||||
var viewportShowToggleFullscreen = getParam('show-toggle-fullscreen', '[^&]+').trim() === '1';
|
||||
|
||||
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
|
||||
|
||||
@@ -73,6 +74,7 @@
|
||||
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
viewportShowToggleFullscreen: viewportShowToggleFullscreen,
|
||||
collapseLeftPanel: collapseLeftPanel,
|
||||
pdbProvider: pdbProvider || 'pdbe',
|
||||
emdbProvider: emdbProvider || 'pdbe',
|
||||
@@ -87,7 +89,7 @@
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
illumination: illumination,
|
||||
resolutionMode: resolutionMode || 'auto'
|
||||
resolutionMode: resolutionMode || 'auto',
|
||||
}).then(viewer => {
|
||||
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
|
||||
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
|
||||
|
||||
@@ -29,7 +29,7 @@ async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderRe
|
||||
const isGz = /\.gz$/i.test(filename);
|
||||
if (filename.match(/\.bcif/)) {
|
||||
let input = await readFileAsync(filename);
|
||||
if (isGz) input = await unzipAsync(input);
|
||||
if (isGz) input = await unzipAsync(input) as NonSharedBuffer;
|
||||
return await CIF.parseBinary(new Uint8Array(input)).runInContext(ctx);
|
||||
} else {
|
||||
const data = isGz ? await unzipAsync(await readFileAsync(filename)) : await readFileAsync(filename);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-validation';
|
||||
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ function paramInfo(param: PD.Any, offset: number): string {
|
||||
}
|
||||
}
|
||||
|
||||
function oToS(options: readonly (readonly [string, string] | readonly [string, string, string | undefined])[]) {
|
||||
function oToS(options: readonly PD.SelectOption<any>[]) {
|
||||
return options.map(o => `'${o[0]}'`).join(', ');
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
@@ -38,7 +39,7 @@ function print(volume: Volume) {
|
||||
}
|
||||
|
||||
async function doMesh(volume: Volume, filename: string) {
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
|
||||
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5), wrap: 'auto' })).run();
|
||||
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
|
||||
|
||||
// Export the mesh in OBJ format.
|
||||
|
||||
@@ -30,8 +30,8 @@ import { ExampleMol } from './example-data';
|
||||
import './index.html';
|
||||
import { jsonCifToMolfile } from './molfile';
|
||||
import { RGroupName } from './r-groups';
|
||||
import { SingleTaskQueue } from './utils';
|
||||
import { molfileToJSONCif } from '../../extensions/json-cif/utils';
|
||||
import { SingleTaskQueue } from '../../mol-util/single-task-queue';
|
||||
|
||||
async function init(target: HTMLElement | string, molfile: string = ExampleMol) {
|
||||
const root = typeof target === 'string' ? document.getElementById(target)! : target;
|
||||
|
||||
@@ -98,14 +98,14 @@
|
||||
|
||||
<body>
|
||||
<div id="viewer">
|
||||
<mvs-stories-viewer />
|
||||
<mvs-stories-viewer></mvs-stories-viewer>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<div id="select-story" class="select-story"></div>
|
||||
<mvs-stories-snapshot-markdown style="flex-grow: 1;" />
|
||||
<mvs-stories-snapshot-markdown style="flex-grow: 1;"></mvs-stories-snapshot-markdown>
|
||||
</div>
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data" filename="kinase-story.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
<a href="#" id="mvs-data">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/mvs-stories" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
@@ -118,6 +118,7 @@
|
||||
window.initStories();
|
||||
}, 0);
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
407
src/examples/mvs-stories/stories/animation.ts
Normal file
407
src/examples/mvs-stories/stories/animation.ts
Normal file
@@ -0,0 +1,407 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat4 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
const Colors = {
|
||||
'1cbs': '#4577B2' as ColorT,
|
||||
|
||||
'ligand-away': '#F3794C' as ColorT,
|
||||
'ligand-docked': '#B9E3A0' as ColorT,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'Animation Demo',
|
||||
key: 'intro',
|
||||
description: `### Molecular Animation
|
||||
A story showcasing MolViewSpec animation capabilities.
|
||||
|
||||
[\[**🔄 Replay Intro**\]](!play-transition)
|
||||
[\[**⏵ Play Snapshots**\]](!play-snapshots)
|
||||
[\[**⏹ Stop Animation**\]](!stop-animation)
|
||||
|
||||
[\[**➡️ Next Snapshot**\]](!next-snapshot)
|
||||
|
||||
`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
const prims = _1cbs.primitives({
|
||||
ref: 'prims',
|
||||
label_opacity: 0,
|
||||
});
|
||||
prims.label({ text: 'Animation Demo', position: { label_asym_id: 'A' }, label_size: 10 });
|
||||
|
||||
const anim = builder.animation({
|
||||
custom: {
|
||||
molstar_trackball: {
|
||||
name: 'rock',
|
||||
params: { speed: 0.5 },
|
||||
}
|
||||
}
|
||||
});
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
ref: 'prims-opacity',
|
||||
target_ref: 'prims',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: 'label_opacity',
|
||||
start: 1,
|
||||
end: 0.66,
|
||||
});
|
||||
|
||||
|
||||
// Uncomment this to make 2nd frame render much faster
|
||||
// It will cause shader compilation to happen during the 1st snapshot
|
||||
|
||||
// const surface = poly.representation({
|
||||
// type: 'surface',
|
||||
// surface_type: 'gaussian',
|
||||
// }).opacity({ opacity: 0 });
|
||||
|
||||
// _1cbs.component({ selector: 'ligand' })
|
||||
// .representation({ type: 'ball_and_stick' })
|
||||
// .opacity({ opacity: 0 });
|
||||
|
||||
// surface.clip({
|
||||
// ref: 'clip',
|
||||
// type: 'plane',
|
||||
// point: [22.0, 15, 0],
|
||||
// normal: [0, 0, 1],
|
||||
// });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-11.49, -37.05, 15.78],
|
||||
target: [15.85, 17.26, 24.32],
|
||||
up: [-0.88, 0.4, 0.26],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Ligand Docking',
|
||||
description: `Animate ligand moving to the binding site`,
|
||||
linger_duration_ms: 2500,
|
||||
transition_duration_ms: 500,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly, repr] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
repr.colorFromSource({
|
||||
ref: 'residue_colors',
|
||||
schema: 'residue',
|
||||
category_name: 'atom_site',
|
||||
field_name: 'label_comp_id',
|
||||
palette: {
|
||||
kind: 'categorical',
|
||||
missing_color: 'white',
|
||||
colors: {
|
||||
ALA: 'red',
|
||||
ILE: 'white',
|
||||
LYS: 'white',
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const surface = poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian',
|
||||
}).opacity({ opacity: 0.33 });
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({
|
||||
ref: 'xform',
|
||||
translation: [5, 20, -20],
|
||||
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1],
|
||||
rotation_center: 'centroid',
|
||||
})
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({ ref: 'ligand-color', color: 'red' });
|
||||
|
||||
surface.clip({
|
||||
ref: 'clip',
|
||||
type: 'plane',
|
||||
point: [22.0, 15, 0],
|
||||
normal: [0, 0, 1],
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
easing: 'sin-in',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 600,
|
||||
duration_ms: 800,
|
||||
property: ['point', 2],
|
||||
end: 0,
|
||||
easing: 'sin-out',
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'clip',
|
||||
start_ms: 1500,
|
||||
duration_ms: 500,
|
||||
property: ['point', 2],
|
||||
end: 55,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'vec3',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'translation',
|
||||
end: [0, 0, 0],
|
||||
noise_magnitude: 1,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'rotation_matrix',
|
||||
target_ref: 'xform',
|
||||
duration_ms: 2000,
|
||||
property: 'rotation',
|
||||
noise_magnitude: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'ligand-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
end: Colors['ligand-docked'],
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'residue_colors',
|
||||
duration_ms: 2000,
|
||||
property: ['palette', 'colors'],
|
||||
start: {
|
||||
ALA: 'yellow',
|
||||
},
|
||||
end: {
|
||||
ILE: 'blue',
|
||||
LYS: 'purple',
|
||||
},
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-30.63, 77.29, 2.28],
|
||||
target: [19.16, 26.15, 22.82],
|
||||
up: [0.69, 0.71, 0.09],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
},
|
||||
{
|
||||
header: 'Highlight & Opacity',
|
||||
description: `Animate emissive, opacity and transform properties`,
|
||||
linger_duration_ms: 2000,
|
||||
transition_duration_ms: 0,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cbs = structure(builder, '1cbs');
|
||||
const [poly,] = polymer(_1cbs, { color: Colors['1cbs'] });
|
||||
|
||||
poly.representation({
|
||||
type: 'surface',
|
||||
surface_type: 'gaussian'
|
||||
}).opacity({ ref: 'opacity', opacity: 1 }).color({ ref: 'surface-color', color: 'white' });
|
||||
|
||||
_1cbs.component({ selector: 'ligand' })
|
||||
.transform({ ref: 'xform', translation: [0, 0, 0] })
|
||||
.representation({
|
||||
ref: 'repr',
|
||||
type: 'ball_and_stick',
|
||||
custom: {
|
||||
molstar_representation_params: {
|
||||
emissive: 0,
|
||||
}
|
||||
}
|
||||
})
|
||||
.color({ color: Colors['ligand-docked'] });
|
||||
|
||||
const primitives = builder.primitives({
|
||||
ref: 'primitives',
|
||||
instances: [
|
||||
Mat4.identity()
|
||||
],
|
||||
opacity: 0,
|
||||
});
|
||||
|
||||
primitives.ellipsoid({
|
||||
center: [0, 0, 0],
|
||||
radius: [2, 3, 2.5],
|
||||
color: 'red'
|
||||
});
|
||||
|
||||
const anim = builder.animation();
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'repr',
|
||||
duration_ms: 1000,
|
||||
property: ['custom', 'molstar_representation_params', 'emissive'],
|
||||
end: 0.2,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'opacity',
|
||||
duration_ms: 1000,
|
||||
frequency: 2,
|
||||
alternate_direction: true,
|
||||
property: 'opacity',
|
||||
end: 0,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'transform_matrix',
|
||||
target_ref: 'primitives',
|
||||
property: ['instances', 0],
|
||||
translation_start: [20.24, 29.64, 14.85],
|
||||
translation_end: [21.84, 21.71, 27.04],
|
||||
translation_frequency: 4,
|
||||
pivot: [0, 0, 0],
|
||||
rotation_noise_magnitude: 0.2,
|
||||
scale_end: [0.01, 0.01, 0.01],
|
||||
duration_ms: 1000,
|
||||
});
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'scalar',
|
||||
target_ref: 'primitives',
|
||||
duration_ms: 1000,
|
||||
property: 'opacity',
|
||||
end: 1,
|
||||
});
|
||||
|
||||
|
||||
anim.interpolate({
|
||||
kind: 'color',
|
||||
target_ref: 'surface-color',
|
||||
duration_ms: 2000,
|
||||
property: 'color',
|
||||
palette: {
|
||||
kind: 'continuous',
|
||||
colors: ['white', Colors['1cbs'], 'white'],
|
||||
}
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [6.92, 47.17, 10.68],
|
||||
target: [21.79, 22.2, 23.43],
|
||||
up: [0.8, 0.57, 0.2],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
return builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
}
|
||||
|
||||
function polymer(structure: MVSStructure, options?: { color?: ColorT }) {
|
||||
const component = structure.component({ selector: { label_asym_id: 'A' } });
|
||||
const reprensentation = component.representation({ type: 'cartoon' });
|
||||
if (options?.color) {
|
||||
reprensentation.color({ color: options.color });
|
||||
}
|
||||
return [component, reprensentation] as const;
|
||||
}
|
||||
|
||||
function pdbUrl(id: string) {
|
||||
return `https://www.ebi.ac.uk/pdbe/entry-files/download/${id.toLowerCase()}.bcif`;
|
||||
}
|
||||
|
||||
export function buildStory(): MVSData_States {
|
||||
const snapshots = Steps.map((s, i) => {
|
||||
const builder = s.state();
|
||||
if (s.camera) builder.camera(s.camera);
|
||||
|
||||
builder.canvas({
|
||||
custom: {
|
||||
molstar_postprocessing: {
|
||||
enable_outline: true,
|
||||
enable_ssao: true,
|
||||
background: {
|
||||
name: 'horizontalGradient',
|
||||
params: {
|
||||
topColor: 0x777777,
|
||||
bottomColor: 0xffffff,
|
||||
}
|
||||
},
|
||||
// Example with background image:
|
||||
// background: {
|
||||
// name: 'image',
|
||||
// params: {
|
||||
// // URL can also be filename in MVSX archive
|
||||
// source: { name: 'url', params: 'URL' }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const description = i > 0 ? `${s.description}\n\n[Go to start](#intro)` : s.description;
|
||||
|
||||
return builder.getSnapshot({
|
||||
title: s.header,
|
||||
key: s.key,
|
||||
description,
|
||||
description_format: 'markdown',
|
||||
linger_duration_ms: s.linger_duration_ms ?? 500,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1000,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'Animation Showcase',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
186
src/examples/mvs-stories/stories/audio.ts
Normal file
186
src/examples/mvs-stories/stories/audio.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -6,8 +6,14 @@
|
||||
|
||||
import { buildStory as kinase } from './kinase';
|
||||
import { buildStory as tbp } from './tbp';
|
||||
import { buildStory as animation } from './animation';
|
||||
import { buildStory as audio } from './audio';
|
||||
import { buildStory as motm1 } from './motm1';
|
||||
|
||||
export const Stories = [
|
||||
{ id: 'kinase', name: 'BCR-ABL: A Kinase Out of Control', buildStory: kinase },
|
||||
{ id: 'tata', name: 'TATA-Binding Protein and its Role in Transcription Initiation ', buildStory: tbp },
|
||||
{ id: 'motm1', name: 'RCSB PDB Molecule of the Month #1', buildStory: motm1 },
|
||||
{ id: 'animation-example', name: 'Molecular Animation Example', buildStory: animation },
|
||||
{ id: 'audio-example', name: 'Audio Playback Example', buildStory: audio },
|
||||
] as const;
|
||||
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
1020
src/examples/mvs-stories/stories/motm1.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,6 @@ export interface CubeGrid {
|
||||
|
||||
export type CubeGridFormat = ModelFormat<CubeGrid>;
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function CubeGridFormat(grid: CubeGrid): CubeGridFormat {
|
||||
return { name: 'custom grid', kind: 'cube-grid', data: grid };
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -18,7 +18,7 @@ import { StateTransformer } from '../../mol-state';
|
||||
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
|
||||
import { AlphaOrbital, Basis, CubeGrid, CubeGridFormat, isCubeGridData } from './data-model';
|
||||
import { createSphericalCollocationDensityGrid } from './density';
|
||||
import { Tensor } from '../../mol-math/linear-algebra';
|
||||
import { Mat4, Tensor } from '../../mol-math/linear-algebra';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
|
||||
export class BasisAndOrbitals extends PluginStateObject.Create<{ basis: Basis, order: SphericalBasisOrder, orbitals: AlphaOrbital[] }>({ name: 'Basis', typeClass: 'Object' }) { }
|
||||
@@ -114,6 +114,7 @@ export const CreateOrbitalVolume = PluginStateTransform.BuiltIn({
|
||||
}, a.data.orbitals[params.index], plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
instances: [{ transform: Mat4.identity() }],
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
@@ -146,6 +147,7 @@ export const CreateOrbitalDensityVolume = PluginStateTransform.BuiltIn({
|
||||
}, a.data.orbitals, plugin.canvas3d?.webgl).runInContext(ctx);
|
||||
const volume: Volume = {
|
||||
grid: data.grid,
|
||||
instances: [{ transform: Mat4.identity() }],
|
||||
sourceData: CubeGridFormat(data),
|
||||
customProperties: new CustomProperties(),
|
||||
_propertyData: Object.create(null),
|
||||
|
||||
@@ -47,11 +47,12 @@ export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
|
||||
}
|
||||
});
|
||||
|
||||
const RemoveNewline = /\r?\n/g;
|
||||
export function confalPyramidLabel(step: DnatcoTypes.Step) {
|
||||
return `
|
||||
<b>${step.auth_asym_id_1}</b> |
|
||||
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
|
||||
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
|
||||
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
|
||||
`;
|
||||
`.replace(RemoveNewline, '');
|
||||
}
|
||||
|
||||
@@ -47,11 +47,12 @@ export const NtCTubePreset = StructureRepresentationPresetProvider({
|
||||
}
|
||||
});
|
||||
|
||||
const RemoveNewline = /\r?\n/g;
|
||||
export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) {
|
||||
return `
|
||||
<b>${step.auth_asym_id_1}</b> |
|
||||
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
|
||||
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
|
||||
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
|
||||
`;
|
||||
`.replace(RemoveNewline, '');
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
private materialMap = new Map<string, number>();
|
||||
private accessors: Record<string, any>[] = [];
|
||||
private bufferViews: Record<string, any>[] = [];
|
||||
private binaryBuffer: ArrayBuffer[] = [];
|
||||
private binaryBuffer: ArrayBufferLike[] = [];
|
||||
private byteOffset = 0;
|
||||
private centerTransform: Mat4;
|
||||
|
||||
@@ -72,7 +72,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return [min, max];
|
||||
}
|
||||
|
||||
private addBuffer(buffer: ArrayBuffer, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
|
||||
private addBuffer(buffer: ArrayBufferLike, componentType: number, type: string, count: number, target: number, min?: any, max?: any, normalized?: boolean) {
|
||||
this.binaryBuffer.push(buffer);
|
||||
|
||||
const bufferViewOffset = this.bufferViews.length;
|
||||
@@ -304,7 +304,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
materials: this.materials
|
||||
};
|
||||
|
||||
const createChunk = (chunkType: number, data: ArrayBuffer[], byteLength: number, padChar: number): [ArrayBuffer[], number] => {
|
||||
const createChunk = (chunkType: number, data: ArrayBufferLike[], byteLength: number, padChar: number): [ArrayBufferLike[], number] => {
|
||||
let padding = null;
|
||||
if (byteLength % 4 !== 0) {
|
||||
const pad = 4 - (byteLength % 4);
|
||||
|
||||
@@ -305,7 +305,7 @@ function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetric
|
||||
let labelNode: ReactNode | undefined;
|
||||
if (label) {
|
||||
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
|
||||
let x: number, y: number, anchor: string;
|
||||
let x: number, y: number, anchor: 'start' | 'end';
|
||||
if (crosshairOffset![0] < PlotSize / 2) {
|
||||
x = PlotOffset + crosshairOffset![0] + 20;
|
||||
anchor = 'start';
|
||||
|
||||
@@ -39,7 +39,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
const format = options?.format ?? 'cif';
|
||||
const { structures } = plugin.managers.structure.hierarchy.current;
|
||||
|
||||
const files: [name: string, data: string | Uint8Array][] = [];
|
||||
const files: [name: string, data: string | Uint8Array<ArrayBuffer>][] = [];
|
||||
const entryMap = new Map<string, number>();
|
||||
|
||||
for (const _s of structures) {
|
||||
@@ -80,7 +80,7 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
if (files.length === 1) {
|
||||
download(new Blob([files[0][1]]), files[0][0]);
|
||||
} else if (files.length > 1) {
|
||||
const zipData: Record<string, Uint8Array> = {};
|
||||
const zipData: Record<string, Uint8Array<ArrayBuffer>> = {};
|
||||
for (const [fn, data] of files) {
|
||||
if (data instanceof Uint8Array) {
|
||||
zipData[fn] = data;
|
||||
|
||||
@@ -25,7 +25,7 @@ export interface Mp4EncoderParams<A extends PluginStateAnimation = PluginStateAn
|
||||
quantizationParameter?: number
|
||||
}
|
||||
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>) {
|
||||
export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin: PluginContext, ctx: RuntimeContext, params: Mp4EncoderParams<A>): Promise<Uint8Array<ArrayBuffer>> {
|
||||
await ctx.update({ message: 'Initializing...', isIndeterminate: true });
|
||||
|
||||
validateViewport(params);
|
||||
@@ -88,7 +88,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
stoppedAnimation = true;
|
||||
encoder.finalize();
|
||||
finalized = true;
|
||||
return encoder.FS.readFile(encoder.outputFilename);
|
||||
return encoder.FS.readFile(encoder.outputFilename) as Uint8Array<ArrayBuffer>;
|
||||
} finally {
|
||||
if (finalized) encoder.delete();
|
||||
if (params.customBackground !== void 0) {
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Mp4AnimationParams, Mp4Controls } from './controls';
|
||||
|
||||
interface State {
|
||||
busy?: boolean,
|
||||
data?: { movie: Uint8Array, filename: string };
|
||||
data?: { movie: Uint8Array<ArrayBuffer>, filename: string };
|
||||
}
|
||||
|
||||
export class Mp4EncoderUI extends CollapsableControls<{}, State> {
|
||||
|
||||
@@ -1 +1 @@
|
||||
Find the MVS extension documentation [here](../../../docs/extensions/mvs/README.md).
|
||||
Please refer to the standalone documentation [here](https://molstar.org/mol-view-spec-docs/).
|
||||
|
||||
@@ -12,9 +12,10 @@ import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRepresentationProvider } from '../../mol-repr/structure/representation';
|
||||
import { StateAction } from '../../mol-state';
|
||||
import { StateAction, StateObject, StateObjectCell, StateTree } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { fileToDataUri } from '../../mol-util/file';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { MVSAnnotationColorThemeProvider } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
@@ -109,6 +110,52 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.add(action);
|
||||
}
|
||||
|
||||
this.ctx.state.data.registerRefResolver('mvs', (state, ref) => {
|
||||
const tagSearch = StateTree.doPreOrder(state.tree, state.tree.root, { ref, ret: undefined as StateObject | undefined }, (n, _, s) => {
|
||||
if (!n.tags) return;
|
||||
for (const t of n.tags) {
|
||||
if (t.startsWith('mvs-ref:') && t.substring(8) === ref) {
|
||||
s.ret = state.cells.get(n.ref)?.obj?.data;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
return tagSearch.ret;
|
||||
});
|
||||
|
||||
this.ctx.managers.markdownExtensions.registerRefResolver('mvs', (plugin, refs) => {
|
||||
const mvsRefs = new Set(refs.map(ref => `mvs-ref:${ref}`));
|
||||
return StateTree.doPreOrder(
|
||||
plugin.state.data.tree,
|
||||
plugin.state.data.tree.root,
|
||||
{ mvsRefs, plugin, cells: [] as StateObjectCell[] },
|
||||
(n, _, s) => {
|
||||
if (!n.tags) return;
|
||||
for (const tag of n.tags) {
|
||||
if (!s.mvsRefs.has(tag)) continue;
|
||||
const cell = s.plugin.state.data.cells.get(n.ref);
|
||||
if (cell) {
|
||||
s.cells.push(cell);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}).cells;
|
||||
});
|
||||
|
||||
this.ctx.managers.markdownExtensions.registerUriResolver('mvs', (plugin, uri) => {
|
||||
const { assets } = plugin.managers.asset;
|
||||
const asset = assets.find(a => a.file.name === uri);
|
||||
if (!asset) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return fileToDataUri(asset.file);
|
||||
} catch (e) {
|
||||
console.error(`MVS: Failed to convert asset file to data URI for '${uri}'`, e);
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
}
|
||||
update(p: { autoAttach: boolean }) {
|
||||
const updated = this.params.autoAttach !== p.autoAttach;
|
||||
@@ -146,6 +193,8 @@ export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.remove(action);
|
||||
}
|
||||
this.ctx.state.data.removeRefResolver('mvs');
|
||||
this.ctx.managers.markdownExtensions.removeRefResolver('mvs');
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
|
||||
import { CameraFogParams, Canvas3DParams, Canvas3DProps, DefaultCanvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { TrackballControlsParams } from '../../mol-canvas3d/controls/trackball';
|
||||
import { BackgroundParams } from '../../mol-canvas3d/passes/background';
|
||||
import { BloomParams } from '../../mol-canvas3d/passes/bloom';
|
||||
import { DofParams } from '../../mol-canvas3d/passes/dof';
|
||||
import { OutlineParams } from '../../mol-canvas3d/passes/outline';
|
||||
import { ShadowParams } from '../../mol-canvas3d/passes/shadow';
|
||||
import { SsaoParams } from '../../mol-canvas3d/passes/ssao';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
@@ -15,15 +22,18 @@ import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { fovAdjustedPosition } from '../../mol-util/camera';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { deepClone } from '../../mol-util/object';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { SnapshotMetadata } from './mvs-data';
|
||||
import { MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSAnimationNode } from './tree/animation/animation-tree';
|
||||
import { MolstarNode, MolstarNodeParams } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
const DefaultFocusOptions = {
|
||||
minRadius: 5,
|
||||
minRadius: 1,
|
||||
extraRadius: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
@@ -52,7 +62,15 @@ export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: Mols
|
||||
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
|
||||
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
|
||||
|
||||
const snapshot: Partial<Camera.Snapshot> = {
|
||||
target,
|
||||
position,
|
||||
up,
|
||||
radius,
|
||||
radiusMax: radius,
|
||||
minNear: params.near ?? undefined,
|
||||
};
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
@@ -115,19 +133,102 @@ export function createPluginStateSnapshotCamera(plugin: PluginContext, context:
|
||||
return camera;
|
||||
}
|
||||
|
||||
/** Set canvas properties based on a canvas node params. */
|
||||
export function setCanvas(plugin: PluginContext, params: MolstarNodeParams<'canvas'> | undefined) {
|
||||
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, params));
|
||||
function optionalParams(enable: boolean | undefined, values: any, params: ParamDefinition.Params, fallback: any) {
|
||||
if (typeof enable === 'boolean') {
|
||||
return enable
|
||||
? { name: 'on', params: { ...ParamDefinition.getDefaultValues(params), ...values } }
|
||||
: { name: 'off', params: {} };
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function normalizeBackground(variant: any, prev: any): any {
|
||||
if (!variant) return prev;
|
||||
return ParamDefinition.normalizeParams(BackgroundParams, { variant }, 'children');
|
||||
}
|
||||
|
||||
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, params: MolstarNodeParams<'canvas'> | undefined): Canvas3DProps {
|
||||
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, canvasNode: MolstarNode<'canvas'> | undefined, animationNode: MVSAnimationNode<'animation'> | undefined): Canvas3DProps {
|
||||
const params = canvasNode?.params;
|
||||
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
|
||||
|
||||
const molstar_postprocessing = canvasNode?.custom?.molstar_postprocessing;
|
||||
|
||||
const outline = molstar_postprocessing?.enable_outline;
|
||||
const outlineParams = molstar_postprocessing?.outline_params;
|
||||
|
||||
const shadow = molstar_postprocessing?.enable_shadow;
|
||||
const shadowParams = molstar_postprocessing?.shadow_params;
|
||||
|
||||
const occlusion = molstar_postprocessing?.enable_ssao;
|
||||
const occlusionParams = molstar_postprocessing?.ssao_params;
|
||||
|
||||
const fog = molstar_postprocessing?.enable_fog;
|
||||
const fogParams = molstar_postprocessing?.fog_params;
|
||||
|
||||
const dof = molstar_postprocessing?.enable_depth_of_field;
|
||||
const dofParams = molstar_postprocessing?.depth_of_field_params;
|
||||
|
||||
const bloom = molstar_postprocessing?.enable_bloom;
|
||||
const bloomParams = molstar_postprocessing?.bloom_params;
|
||||
|
||||
const background = molstar_postprocessing?.background;
|
||||
|
||||
const trackballAnimation = animationNode?.custom?.molstar_trackball;
|
||||
const trackballAnimationName = trackballAnimation?.name;
|
||||
const trackballAnimationParams = trackballAnimation?.params ?? {};
|
||||
|
||||
return {
|
||||
...oldCanvasProps,
|
||||
postprocessing: {
|
||||
...oldCanvasProps.postprocessing,
|
||||
outline: optionalParams(outline, outlineParams, OutlineParams, oldCanvasProps.postprocessing.outline),
|
||||
shadow: optionalParams(shadow, shadowParams, ShadowParams, oldCanvasProps.postprocessing.shadow),
|
||||
occlusion: optionalParams(occlusion, occlusionParams, SsaoParams, oldCanvasProps.postprocessing.occlusion),
|
||||
dof: optionalParams(dof, dofParams, DofParams, oldCanvasProps.postprocessing.dof),
|
||||
bloom: optionalParams(bloom, bloomParams, BloomParams, oldCanvasProps.postprocessing.bloom),
|
||||
background: normalizeBackground(background, oldCanvasProps.postprocessing.background),
|
||||
},
|
||||
cameraFog: optionalParams(fog, fogParams, CameraFogParams, oldCanvasProps.cameraFog),
|
||||
renderer: {
|
||||
...oldCanvasProps.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
},
|
||||
trackball: {
|
||||
...oldCanvasProps?.trackball,
|
||||
...(trackballAnimationName
|
||||
? {
|
||||
animate: {
|
||||
name: trackballAnimationName,
|
||||
params: {
|
||||
...TrackballControlsParams.animate.map(trackballAnimationName)?.defaultValue,
|
||||
...trackballAnimationParams
|
||||
}
|
||||
}
|
||||
}
|
||||
: {}
|
||||
),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function resetCanvasProps(plugin: PluginContext) {
|
||||
const old = plugin.canvas3d?.props;
|
||||
plugin.canvas3d?.setProps({
|
||||
...old,
|
||||
postprocessing: {
|
||||
...old,
|
||||
outline: deepClone(DefaultCanvas3DParams.postprocessing.outline),
|
||||
shadow: deepClone(DefaultCanvas3DParams.postprocessing.shadow),
|
||||
occlusion: deepClone(DefaultCanvas3DParams.postprocessing.occlusion),
|
||||
dof: deepClone(DefaultCanvas3DParams.postprocessing.dof),
|
||||
bloom: deepClone(DefaultCanvas3DParams.postprocessing.bloom),
|
||||
background: deepClone(DefaultCanvas3DParams.postprocessing.background),
|
||||
},
|
||||
cameraFog: deepClone(DefaultCanvas3DParams.cameraFog),
|
||||
trackball: {
|
||||
...old?.trackball,
|
||||
animate: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,32 +1,85 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import type { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import type { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MaybeFloatParamDefinition } from '../helpers/param-definition';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
import { getMVSAnnotationForStructure } from './annotation-prop';
|
||||
import { getMVSAnnotationForStructure, MVSAnnotation } from './annotation-prop';
|
||||
import { isMVSStructure } from './is-mvs-model-prop';
|
||||
|
||||
|
||||
export const MVSCategoricalPaletteParams = {
|
||||
colors: PD.MappedStatic('list', {
|
||||
list: PD.ColorList('category-10', { description: 'List of colors.', presetKind: 'set' }),
|
||||
dictionary: PD.ObjectList({
|
||||
value: PD.Text(),
|
||||
color: PD.Color(ColorNames.white),
|
||||
}, e => `${e.value}: ${Color.toHexStyle(e.color)}`, { description: 'Mapping of annotation values to colors.' }),
|
||||
}),
|
||||
repeatColorList: PD.Boolean(false, { hideIf: g => g.colors.name !== 'list', description: 'Repeat color list once all colors are depleted (only applies if `colors` is a list).' }),
|
||||
sort: PD.Select('none', [['none', 'None'], ['lexical', 'Lexical'], ['numeric', 'Numeric']] as const, { hideIf: g => g.colors.name !== 'list', description: 'Sort actual annotation values before assigning colors from a list (none = take values in order of their first occurrence).' }),
|
||||
sortDirection: PD.Select('ascending', [['ascending', 'Ascending'], ['descending', 'Descending']] as const, { hideIf: g => g.colors.name !== 'list', description: 'Sort direction.' }),
|
||||
caseInsensitive: PD.Boolean(false, { description: 'Treat annotation values as case-insensitive strings.' }),
|
||||
setMissingColor: PD.Boolean(false, { description: 'Allow setting a color for missing values.' }),
|
||||
missingColor: PD.Color(ColorNames.white, { hideIf: g => !g.setMissingColor, description: 'Color to use when (a) `colors` is a dictionary and given key is not present, or (b) `color` is a list and there are more actual annotation values than listed colors and `repeat_color_list` is not true.' }),
|
||||
};
|
||||
export type MVSCategoricalPaletteParams = typeof MVSCategoricalPaletteParams
|
||||
export type MVSCategoricalPaletteProps = PD.Values<MVSCategoricalPaletteParams>
|
||||
|
||||
export const MVSDiscretePaletteParams = {
|
||||
colors: PD.ObjectList({
|
||||
color: PD.Color(ColorNames.white),
|
||||
fromValue: PD.Numeric(-Infinity),
|
||||
toValue: PD.Numeric(Infinity),
|
||||
}, e => `${Color.toHexStyle(e.color)} [${e.fromValue}, ${e.toValue}]`, { description: 'Mapping of annotation value ranges to colors.' }),
|
||||
mode: PD.Select('normalized', [['normalized', 'Normalized'], ['absolute', 'Absolute']] as const, { description: 'Defines whether the annotation values should be normalized before assigning color based on checkpoints in `colors` (`x_normalized = (x - x_min) / (x_max - x_min)`, where `[x_min, x_max]` are either `value_domain` if provided, or the lowest and the highest value encountered in the annotation).' }),
|
||||
xMin: MaybeFloatParamDefinition({ hideIf: g => g.mode !== 'normalized', placeholder: 'auto', description: 'Defines `x_min` for normalization of annotation values. If not provided, minimum of the actual values will be used. Only used when `mode` is `"normalized"' }),
|
||||
xMax: MaybeFloatParamDefinition({ hideIf: g => g.mode !== 'normalized', placeholder: 'auto', description: 'Defines `x_max` for normalization of annotation values. If not provided, maximum of the actual values will be used. Only used when `mode` is `"normalized"' }),
|
||||
};
|
||||
export type MVSDiscretePaletteParams = typeof MVSDiscretePaletteParams
|
||||
export type MVSDiscretePaletteProps = PD.Values<MVSDiscretePaletteParams>
|
||||
|
||||
export const MVSContinuousPaletteParams = {
|
||||
colors: PD.ColorList('yellow-green', { description: 'List of colors, with optional checkpoints.', presetKind: 'scale', offsets: true }),
|
||||
mode: PD.Select('normalized', [['normalized', 'Normalized'], ['absolute', 'Absolute']] as const, { description: 'Defines whether the annotation values should be normalized before assigning color based on checkpoints in `colors` (`x_normalized = (x - x_min) / (x_max - x_min)`, where `[x_min, x_max]` are either `value_domain` if provided, or the lowest and the highest value encountered in the annotation).' }),
|
||||
xMin: MaybeFloatParamDefinition({ hideIf: g => g.mode !== 'normalized', placeholder: 'auto', description: 'Defines `x_min` for normalization of annotation values. If not provided, minimum of the actual values will be used. Only used when `mode` is `"normalized"' }),
|
||||
xMax: MaybeFloatParamDefinition({ hideIf: g => g.mode !== 'normalized', placeholder: 'auto', description: 'Defines `x_max` for normalization of annotation values. If not provided, maximum of the actual values will be used. Only used when `mode` is `"normalized"' }),
|
||||
setUnderflowColor: PD.Boolean(false, { description: 'Allow setting a color for values below the lowest checkpoint.' }),
|
||||
underflowColor: PD.Color(ColorNames.white, { hideIf: g => !g.setUnderflowColor, description: 'Color for values below the lowest checkpoint.' }),
|
||||
setOverflowColor: PD.Boolean(false, { description: 'Allow setting a color for values above the highest checkpoint.' }),
|
||||
overflowColor: PD.Color(ColorNames.white, { hideIf: g => !g.setOverflowColor, description: 'Color for values above the highest checkpoint.' }),
|
||||
};
|
||||
export type MVSContinuousPaletteParams = typeof MVSContinuousPaletteParams
|
||||
export type MVSContinuousPaletteProps = PD.Values<MVSContinuousPaletteParams>
|
||||
|
||||
|
||||
/** Parameter definition for color theme "MVS Annotation" */
|
||||
export const MVSAnnotationColorThemeParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property' }),
|
||||
fieldName: PD.Text('color', { description: 'Annotation field (column) from which to take color values' }),
|
||||
background: PD.Color(ColorNames.gainsboro, { description: 'Color for elements without annotation' }),
|
||||
palette: PD.MappedStatic('direct', {
|
||||
'direct': PD.EmptyGroup(),
|
||||
'categorical': PD.Group(MVSCategoricalPaletteParams),
|
||||
'discrete': PD.Group(MVSDiscretePaletteParams),
|
||||
'continuous': PD.Group(MVSContinuousPaletteParams),
|
||||
}),
|
||||
};
|
||||
export type MVSAnnotationColorThemeParams = typeof MVSAnnotationColorThemeParams
|
||||
|
||||
/** Parameter values for color theme "MVS Annotation" */
|
||||
export type MVSAnnotationColorThemeProps = PD.Values<MVSAnnotationColorThemeParams>
|
||||
|
||||
|
||||
/** Return color theme that assigns colors based on an annotation file.
|
||||
* The annotation file itself is handled by a custom model property (`MVSAnnotationsProvider`),
|
||||
* the color theme then just uses this property. */
|
||||
@@ -36,9 +89,12 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
|
||||
if (ctx.structure && !ctx.structure.isEmpty) {
|
||||
const { annotation } = getMVSAnnotationForStructure(ctx.structure, props.annotationId);
|
||||
if (annotation) {
|
||||
const paletteFunction = makePaletteFunction(props.palette, annotation, props.fieldName);
|
||||
|
||||
const colorForStructureElementLocation = (location: StructureElement.Location) => {
|
||||
// if (annot.getAnnotationForLocation(location)?.color !== annot.getAnnotationForLocation_Reference(location)?.color) throw new Error('AssertionError');
|
||||
return decodeColor(annotation?.getValueForLocation(location, props.fieldName)) ?? props.background;
|
||||
const annotValue = annotation?.getValueForLocation(location, props.fieldName);
|
||||
const color = annotValue !== undefined ? paletteFunction(annotValue) : undefined;
|
||||
return color ?? props.background;
|
||||
};
|
||||
const auxLocation = StructureElement.Location.create(ctx.structure);
|
||||
|
||||
@@ -60,8 +116,8 @@ export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotat
|
||||
|
||||
return {
|
||||
factory: MVSAnnotationColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
granularity: 'groupInstance',
|
||||
preferSmoothing: false,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Assigns colors based on custom MolViewSpec annotation data.',
|
||||
@@ -79,3 +135,124 @@ export const MVSAnnotationColorThemeProvider: ColorTheme.Provider<MVSAnnotationC
|
||||
defaultValues: PD.getDefaultValues(MVSAnnotationColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && isMVSStructure(ctx.structure),
|
||||
};
|
||||
|
||||
|
||||
function makePaletteFunction(props: MVSAnnotationColorThemeProps['palette'], annotation: MVSAnnotation, fieldName: string): (value: string) => Color | undefined {
|
||||
if (props.name === 'direct') return decodeColor;
|
||||
if (props.name === 'categorical') return makePaletteFunctionCategorical(props.params, annotation, fieldName);
|
||||
if (props.name === 'discrete') return makePaletteFunctionDiscrete(props.params as MVSDiscretePaletteProps, annotation, fieldName);
|
||||
if (props.name === 'continuous') return makePaletteFunctionContinuous(props.params as MVSContinuousPaletteProps, annotation, fieldName);
|
||||
throw new Error(`NotImplementedError: makePaletteFunction for ${(props as any).name}`);
|
||||
}
|
||||
|
||||
function makePaletteFunctionCategorical(props: MVSCategoricalPaletteProps, annotation: MVSAnnotation, fieldName: string): (value: string) => Color | undefined {
|
||||
const colorMap: { [value: string]: Color } = {};
|
||||
if (props.colors.name === 'dictionary') {
|
||||
for (const { value, color } of props.colors.params) {
|
||||
const key = props.caseInsensitive ? value.toUpperCase() : value;
|
||||
colorMap[key] = color;
|
||||
}
|
||||
} else if (props.colors.name === 'list') {
|
||||
const values = annotation.getDistinctValuesInField(fieldName, props.caseInsensitive);
|
||||
if (props.sort === 'lexical') values.sort();
|
||||
else if (props.sort === 'numeric') values.sort((a, b) => Number.parseFloat(a) - Number.parseFloat(b));
|
||||
if (props.sortDirection === 'descending') values.reverse();
|
||||
|
||||
const colorList = props.colors.params.colors.map(Color.fromColorListEntry);
|
||||
let next = 0;
|
||||
for (const value of values) {
|
||||
colorMap[value] = colorList[next++];
|
||||
if (next >= colorList.length && props.repeatColorList) next = 0; // else will get index-out-of-range and assign undefined
|
||||
}
|
||||
}
|
||||
const missingColor = props.setMissingColor ? props.missingColor : undefined;
|
||||
if (props.caseInsensitive) {
|
||||
return (value: string) => colorMap[value.toUpperCase()] ?? missingColor;
|
||||
} else {
|
||||
return (value: string) => colorMap[value] ?? missingColor;
|
||||
}
|
||||
}
|
||||
|
||||
function makePaletteFunctionDiscrete(props: MVSDiscretePaletteProps, annotation: MVSAnnotation, fieldName: string): (value: string) => Color | undefined {
|
||||
if (props.colors.length === 0) return () => undefined;
|
||||
|
||||
const scale = makeNumericPaletteScale(props, annotation, fieldName);
|
||||
|
||||
return (value: string) => {
|
||||
const xAbs = parseFloat(value);
|
||||
if (isNaN(xAbs)) return undefined;
|
||||
const x = scale(xAbs);
|
||||
|
||||
for (let i = props.colors.length - 1; i >= 0; i--) {
|
||||
const { color, fromValue, toValue } = props.colors[i];
|
||||
if (fromValue <= x && x <= toValue) return color;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function makePaletteFunctionContinuous(props: MVSContinuousPaletteProps, annotation: MVSAnnotation, fieldName: string): (value: string) => Color | undefined {
|
||||
const { colors, checkpoints } = makeContinuousPaletteCheckpoints(props);
|
||||
if (colors.length === 0) return () => undefined;
|
||||
|
||||
const scale = makeNumericPaletteScale(props, annotation, fieldName);
|
||||
const underflowColor = props.setUnderflowColor ? props.underflowColor : undefined;
|
||||
const overflowColor = props.setOverflowColor ? props.overflowColor : undefined;
|
||||
|
||||
return (value: string) => {
|
||||
const xAbs = parseFloat(value);
|
||||
if (isNaN(xAbs)) return undefined;
|
||||
const x = scale(xAbs);
|
||||
const gteIdx = SortedArray.findPredecessorIndex(checkpoints, x); // Index of the first greater or equal checkpoint
|
||||
if (gteIdx === 0) {
|
||||
if (x === checkpoints[0]) return colors[0];
|
||||
else return underflowColor;
|
||||
}
|
||||
if (gteIdx === checkpoints.length) {
|
||||
return overflowColor;
|
||||
}
|
||||
const q = (x - checkpoints[gteIdx - 1]) / (checkpoints[gteIdx] - checkpoints[gteIdx - 1]);
|
||||
return Color.interpolate(colors[gteIdx - 1], colors[gteIdx], q);
|
||||
};
|
||||
}
|
||||
|
||||
function makeNumericPaletteScale(props: MVSContinuousPaletteProps | MVSDiscretePaletteProps, annotation: MVSAnnotation, fieldName: string): (x: number) => number {
|
||||
if (props.mode === 'normalized') {
|
||||
// Mode normalized
|
||||
let xMin = props.xMin;
|
||||
let xMax = props.xMax;
|
||||
if (xMin === null || xMax === null) {
|
||||
const values = annotation.getDistinctValuesInField(fieldName, false).map(parseFloat).filter(x => !isNaN(x));
|
||||
if (values.length > 0) {
|
||||
xMin ??= values.reduce((a, b) => a < b ? a : b); // xMin ??= min(values)
|
||||
xMax ??= values.reduce((a, b) => a > b ? a : b); // xMax ??= max(values)
|
||||
} else {
|
||||
xMin ??= 0;
|
||||
xMax ??= 1;
|
||||
}
|
||||
}
|
||||
if (xMin === xMax) {
|
||||
return x => (x < xMin ? -0.5 : x === xMin ? 0.5 : 1.5);
|
||||
} else {
|
||||
return x => (x - xMin) / (xMax - xMin);
|
||||
}
|
||||
} else {
|
||||
// Mode absolute
|
||||
return x => x;
|
||||
}
|
||||
}
|
||||
|
||||
export function makeContinuousPaletteCheckpoints(props: MVSContinuousPaletteProps) {
|
||||
if (props.colors.colors.every(x => Array.isArray(x))) {
|
||||
// Explicit checkpoints
|
||||
const sorted = props.colors.colors.sort((a, b) => a[1] - b[1]);
|
||||
const colors = sorted.map(Color.fromColorListEntry);
|
||||
const checkpoints = SortedArray.ofSortedArray(sorted.map(t => t[1]));
|
||||
return { colors, checkpoints };
|
||||
} else {
|
||||
// Auto checkpoints (linspace 0 to 1)
|
||||
const colors = props.colors.colors.map(Color.fromColorListEntry);
|
||||
const n = colors.length - 1;
|
||||
const checkpoints = SortedArray.ofSortedArray(colors.map((_, i) => i / n));
|
||||
return { colors, checkpoints };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -57,7 +57,7 @@ function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme,
|
||||
const labelText = annotation!.getValueForRow(iFirstRowInGroup, props.fieldName);
|
||||
if (!labelText) continue;
|
||||
const rowsInGroup = grouped.slice(offsets[iGroup], offsets[iGroup + 1]).map(j => rows[j]);
|
||||
const p = textPropsForSelection(structure, theme.size.size, rowsInGroup, model);
|
||||
const p = textPropsForSelection(structure, rowsInGroup, model);
|
||||
if (!p) continue;
|
||||
builder.add(labelText, p.center[0], p.center[1], p.center[2], p.depth, p.scale, p.group);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../../mol-data/db';
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { CIF, CifBlock, CifCategory, CifFile } from '../../../mol-io/reader/cif';
|
||||
import { toTable } from '../../../mol-io/reader/cif/schema';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
@@ -12,20 +12,18 @@ import { CustomModelProperty } from '../../../mol-model-props/common/custom-mode
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure/structure';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayExtend } from '../../../mol-util/array';
|
||||
import { Structure, StructureElement, Unit } from '../../../mol-model/structure/structure';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Jsonable, canonicalJsonString } from '../../../mol-util/json';
|
||||
import { pickObjectKeys, promiseAllObj } from '../../../mol-util/object';
|
||||
import { objectOfArraysToArrayOfObjects, pickObjectKeysWithRemapping, promiseAllObj } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { AtomRanges } from '../helpers/atom-ranges';
|
||||
import { ElementRanges } from '../helpers/element-ranges';
|
||||
import { IndicesAndSortings } from '../helpers/indexing';
|
||||
import { MaybeStringParamDefinition } from '../helpers/param-definition';
|
||||
import { MVSAnnotationRow, MVSAnnotationSchema, getCifAnnotationSchema } from '../helpers/schemas';
|
||||
import { atomQualifies, getAtomRangesForRow } from '../helpers/selections';
|
||||
import { Maybe, safePromise } from '../helpers/utils';
|
||||
import { getAtomRangesForRow, getGaussianRangesForRow, getSphereRangesForRow } from '../helpers/selections';
|
||||
import { Maybe, isDefined, safePromise } from '../helpers/utils';
|
||||
|
||||
|
||||
/** Allowed values for the annotation format parameter */
|
||||
@@ -50,7 +48,11 @@ export const MVSAnnotationsParams = {
|
||||
index: PD.Group({ index: PD.Numeric(0, { min: 0, step: 1 }, { description: '0-based index of the block' }) }),
|
||||
header: PD.Group({ header: PD.Text(undefined, { description: 'Block header' }) }),
|
||||
}, { description: 'Specify which CIF block contains annotation data (only relevant when format=cif or format=bcif)' }),
|
||||
cifCategory: MaybeStringParamDefinition(undefined, { description: 'Specify which CIF category contains annotation data (only relevant when format=cif or format=bcif)' }),
|
||||
cifCategory: MaybeStringParamDefinition({ placeholder: 'Take first category', description: 'Specify which CIF category contains annotation data (only relevant when format=cif or format=bcif)' }),
|
||||
fieldRemapping: PD.ObjectList({
|
||||
standardName: PD.Text('', { placeholder: ' ', description: 'Standard name of the selector field (e.g. label_asym_id)' }),
|
||||
actualName: MaybeStringParamDefinition({ placeholder: 'Ignore field', description: 'Actual name of the field in the annotation data (e.g. spam_chain_id), null to ignore the field with standard name' }),
|
||||
}, e => `"${e.standardName}": ${e.actualName === null ? 'null' : `"${e.actualName}"`}`, { description: 'Optional remapping of annotation field names { standardName1: actualName1, ... }. Use { "label_asym_id": "X" } to load actual field "X" as "label_asym_id". Use { "label_asym_id": null } to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name).' }),
|
||||
id: PD.Text('', { description: 'Arbitrary identifier that can be referenced by MVSAnnotationColorTheme' }),
|
||||
},
|
||||
obj => obj.id
|
||||
@@ -137,16 +139,41 @@ export function getMVSAnnotationForStructure(structure: Structure, annotationId:
|
||||
return { annotation: undefined, model: undefined };
|
||||
}
|
||||
|
||||
type FieldRemapping = Record<string, string | null>;
|
||||
|
||||
/** Mapping `ElementIndex` -> annotation row index for all elements of one kind (atoms, spheres, gaussians) in a `Model`.
|
||||
* `-1` means no row applies to the element.
|
||||
* `null` means no row applies to any element. */
|
||||
type IndexedElements = number[] | null;
|
||||
|
||||
/** Mapping `ElementIndex` -> annotation row index for atoms, spheres, and gaussians in a `Model`. */
|
||||
type IndexedModel = {
|
||||
atoms: IndexedElements,
|
||||
spheres: IndexedElements,
|
||||
gaussians: IndexedElements,
|
||||
};
|
||||
|
||||
function getIndexedElementsForUnitKind(indexedModel: IndexedModel, unitKind: Unit.Kind): IndexedElements {
|
||||
if (unitKind === Unit.Kind.Atomic) return indexedModel.atoms;
|
||||
if (unitKind === Unit.Kind.Spheres) return indexedModel.spheres;
|
||||
if (unitKind === Unit.Kind.Gaussians) return indexedModel.gaussians;
|
||||
console.warn(`Unknown Unit.Kind value: ${unitKind}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Main class for processing MVS annotation */
|
||||
export class MVSAnnotation {
|
||||
/** Store mapping `ElementIndex` -> annotation row index for each `Model`, -1 means no row applies */
|
||||
private indexedModels = new Map<UUID, number[]>();
|
||||
private rows: MVSAnnotationRow[] | undefined = undefined;
|
||||
|
||||
/** Number of annotation rows. */
|
||||
public nRows: number;
|
||||
|
||||
constructor(
|
||||
public data: MVSAnnotationData,
|
||||
public schema: MVSAnnotationSchema,
|
||||
) { }
|
||||
public fieldRemapping: FieldRemapping,
|
||||
) {
|
||||
this.nRows = getRowCount(data);
|
||||
}
|
||||
|
||||
/** Create a new `MVSAnnotation` based on specification `spec`. Use `file` if provided, otherwise download the file.
|
||||
* Throw error if download fails or problem with data. */
|
||||
@@ -165,7 +192,7 @@ export class MVSAnnotation {
|
||||
switch (blockSpec.name) {
|
||||
case 'header':
|
||||
const foundBlock = file.data.blocks.find(b => b.header === blockSpec.params.header);
|
||||
if (!foundBlock) throw new Error(`CIF block with header ${blockSpec.params.header} not found`);
|
||||
if (!foundBlock) throw new Error(`CIF block with header "${blockSpec.params.header}" not found`);
|
||||
block = foundBlock;
|
||||
break;
|
||||
case 'index':
|
||||
@@ -176,32 +203,22 @@ export class MVSAnnotation {
|
||||
const categoryName = spec.cifCategory ?? Object.keys(block.categories)[0];
|
||||
if (!categoryName) throw new Error('There are no categories in CIF block');
|
||||
const category = block.categories[categoryName];
|
||||
if (!category) throw new Error(`CIF category ${categoryName} not found`);
|
||||
if (!category) throw new Error(`CIF category "${categoryName}" not found`);
|
||||
data = { format: 'cif', data: category };
|
||||
break;
|
||||
}
|
||||
return new MVSAnnotation(data, spec.schema);
|
||||
return new MVSAnnotation(data, spec.schema, Object.fromEntries(spec.fieldRemapping.map(e => [e.standardName, e.actualName])));
|
||||
}
|
||||
|
||||
static createEmpty(schema: MVSAnnotationSchema): MVSAnnotation {
|
||||
return new MVSAnnotation({ format: 'json', data: [] }, schema);
|
||||
}
|
||||
|
||||
/** Reference implementation of `getAnnotationForLocation`, just for checking, DO NOT USE DIRECTLY */
|
||||
getAnnotationForLocation_Reference(loc: StructureElement.Location): MVSAnnotationRow | undefined {
|
||||
const model = loc.unit.model;
|
||||
const iAtom = loc.element;
|
||||
let result: MVSAnnotationRow | undefined = undefined;
|
||||
for (const row of this.getRows()) {
|
||||
if (atomQualifies(model, iAtom, row)) result = row;
|
||||
}
|
||||
return result;
|
||||
return new MVSAnnotation({ format: 'json', data: [] }, schema, {});
|
||||
}
|
||||
|
||||
/** Return value of field `fieldName` assigned to location `loc`, if any */
|
||||
getValueForLocation(loc: StructureElement.Location, fieldName: string): string | undefined {
|
||||
const indexedModel = this.getIndexedModel(loc.unit.model);
|
||||
const iRow = indexedModel[loc.element];
|
||||
const indexedModel = this.getIndexedModel(loc.unit.model, loc.unit.conformation.operator.instanceId);
|
||||
const indexedElements = getIndexedElementsForUnitKind(indexedModel, loc.unit.kind);
|
||||
const iRow = indexedElements ? indexedElements[loc.element] : -1;
|
||||
return this.getValueForRow(iRow, fieldName);
|
||||
}
|
||||
/** Return value of field `fieldName` assigned to `i`-th annotation row, if any */
|
||||
@@ -218,40 +235,75 @@ export class MVSAnnotation {
|
||||
}
|
||||
|
||||
/** Return cached `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` (or create it if not cached yet) */
|
||||
private getIndexedModel(model: Model): number[] {
|
||||
const key = model.id;
|
||||
if (!this.indexedModels.has(key)) {
|
||||
const result = this.getRowForEachAtom(model);
|
||||
this.indexedModels.set(key, result);
|
||||
private getIndexedModel(model: Model, instanceId: string): IndexedModel {
|
||||
const key = this.hasInstanceIds() ? `${model.id}:${instanceId}` : model.id;
|
||||
if (!this._indexedModels.has(key)) {
|
||||
const result = this.getRowForEachAtom(model, instanceId);
|
||||
this._indexedModels.set(key, result);
|
||||
}
|
||||
return this.indexedModels.get(key)!;
|
||||
return this._indexedModels.get(key)!;
|
||||
}
|
||||
/** Cached `IndexedModel` per `Model.id` (if annotation contains no instanceIds)
|
||||
* or per `Model.id:instanceId` combination (if at least one row contains instanceId). */
|
||||
private _indexedModels = new Map<string, IndexedModel>();
|
||||
|
||||
/** Create `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` */
|
||||
private getRowForEachAtom(model: Model): number[] {
|
||||
private getRowForEachAtom(model: Model, instanceId: string): IndexedModel {
|
||||
const indices = IndicesAndSortings.get(model);
|
||||
const nAtoms = model.atomicHierarchy.atoms._rowCount;
|
||||
const result: number[] = Array(nAtoms).fill(-1);
|
||||
const nSpheres = model.coarseHierarchy.spheres.count;
|
||||
const nGaussians = model.coarseHierarchy.gaussians.count;
|
||||
let indexedAtoms: IndexedElements = null;
|
||||
let indexedSpheres: IndexedElements = null;
|
||||
let indexedGaussians: IndexedElements = null;
|
||||
const rows = this.getRows();
|
||||
for (let i = 0, nRows = rows.length; i < nRows; i++) {
|
||||
const atomRanges = getAtomRangesForRow(model, rows[i], indices);
|
||||
AtomRanges.foreach(atomRanges, (from, to) => result.fill(i, from, to));
|
||||
for (let iRow = 0, nRows = rows.length; iRow < nRows; iRow++) {
|
||||
const row = rows[iRow];
|
||||
const atomRanges = getAtomRangesForRow(row, model, instanceId, indices);
|
||||
indexedAtoms = fillValueOnRanges(indexedAtoms, nAtoms, atomRanges, iRow);
|
||||
const sphereRanges = getSphereRangesForRow(row, model, instanceId, indices);
|
||||
indexedSpheres = fillValueOnRanges(indexedSpheres, nSpheres, sphereRanges, iRow);
|
||||
const gaussianRanges = getGaussianRangesForRow(row, model, instanceId, indices);
|
||||
indexedGaussians = fillValueOnRanges(indexedGaussians, nGaussians, gaussianRanges, iRow);
|
||||
}
|
||||
return result;
|
||||
return { atoms: indexedAtoms, spheres: indexedSpheres, gaussians: indexedGaussians };
|
||||
}
|
||||
|
||||
/** Parse and return all annotation rows in this annotation, or return cached result if available */
|
||||
getRows(): readonly MVSAnnotationRow[] {
|
||||
return this._rows ??= this._getRows();
|
||||
}
|
||||
/** Cached annotation rows. Do not use directly, use `getRows` instead. */
|
||||
private _rows: MVSAnnotationRow[] | undefined = undefined;
|
||||
/** Parse and return all annotation rows in this annotation */
|
||||
private _getRows(): MVSAnnotationRow[] {
|
||||
switch (this.data.format) {
|
||||
case 'json':
|
||||
return getRowsFromJson(this.data.data, this.schema);
|
||||
return getRowsFromJson(this.data.data, this.schema, this.fieldRemapping);
|
||||
case 'cif':
|
||||
return getRowsFromCif(this.data.data, this.schema);
|
||||
return getRowsFromCif(this.data.data, this.schema, this.fieldRemapping);
|
||||
}
|
||||
}
|
||||
/** Parse and return all annotation rows in this annotation, or return cached result if available */
|
||||
getRows(): readonly MVSAnnotationRow[] {
|
||||
return this.rows ??= this._getRows();
|
||||
|
||||
/** Return `true` if some rows in the annotation contain `instance_id` field. */
|
||||
private hasInstanceIds(): boolean {
|
||||
return this._hasInstanceIds ??= this.getRows().some(row => isDefined(row.instance_id));
|
||||
}
|
||||
private _hasInstanceIds?: boolean = undefined;
|
||||
|
||||
/** Return list of all distinct values appearing in field `fieldName`, in order of first occurrence. Ignores special values `.` and `?`. If `caseInsensitive`, make all values uppercase. */
|
||||
getDistinctValuesInField(fieldName: string, caseInsensitive: boolean): string[] {
|
||||
const seen = new Set<string | undefined>();
|
||||
const out = [];
|
||||
for (let i = 0; i < this.nRows; i++) {
|
||||
let value = this.getValueForRow(i, fieldName);
|
||||
if (caseInsensitive) value = value?.toUpperCase();
|
||||
if (value !== undefined && !seen.has(value)) {
|
||||
seen.add(value);
|
||||
out.push(value);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,55 +324,81 @@ function getValueFromCif(rowIndex: number, fieldName: string, data: CifCategory)
|
||||
return column.str(rowIndex);
|
||||
}
|
||||
|
||||
function getRowsFromJson(data: Jsonable, schema: MVSAnnotationSchema): MVSAnnotationRow[] {
|
||||
/** Return number of rows in this annotation (without parsing all the data) */
|
||||
function getRowCount(data: MVSAnnotationData): number {
|
||||
switch (data.format) {
|
||||
case 'json':
|
||||
return getRowCountFromJson(data.data);
|
||||
case 'cif':
|
||||
return getRowCountFromCif(data.data);
|
||||
}
|
||||
}
|
||||
function getRowCountFromJson(data: Jsonable): number {
|
||||
const js = data as any;
|
||||
const cifSchema = getCifAnnotationSchema(schema);
|
||||
if (Array.isArray(js)) {
|
||||
// array of objects
|
||||
return js.map(row => pickObjectKeys(row, Object.keys(cifSchema)));
|
||||
return js.length;
|
||||
} else {
|
||||
// object of arrays
|
||||
const rows: MVSAnnotationRow[] = [];
|
||||
const keys = Object.keys(js).filter(key => Object.hasOwn(cifSchema, key as any));
|
||||
const keys = Object.keys(js);
|
||||
if (keys.length > 0) {
|
||||
const n = js[keys[0]].length;
|
||||
if (keys.some(key => js[key].length !== n)) throw new Error('FormatError: arrays must have the same length.');
|
||||
for (let i = 0; i < n; i++) {
|
||||
const item: { [key: string]: any } = {};
|
||||
for (const key of keys) {
|
||||
item[key] = js[key][i];
|
||||
}
|
||||
rows.push(item);
|
||||
}
|
||||
return js[keys[0]].length;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
function getRowCountFromCif(data: CifCategory): number {
|
||||
return data.rowCount;
|
||||
}
|
||||
|
||||
function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema): MVSAnnotationRow[] {
|
||||
const rows: MVSAnnotationRow[] = [];
|
||||
function getRowsFromJson(data: Jsonable, schema: MVSAnnotationSchema, fieldRemapping: FieldRemapping): MVSAnnotationRow[] {
|
||||
const js = data as any;
|
||||
const cifSchema = getCifAnnotationSchema(schema);
|
||||
const table = toTable(cifSchema, data);
|
||||
arrayExtend(rows, getRowsFromTable(table)); // Avoiding Table.getRows(table) as it replaces . and ? fields by 0 or ''
|
||||
return rows;
|
||||
const cifSchemaKeys = Object.keys(cifSchema);
|
||||
if (Array.isArray(js)) {
|
||||
// array of objects
|
||||
return js.map(row => pickObjectKeysWithRemapping(row, cifSchemaKeys, fieldRemapping));
|
||||
} else {
|
||||
// object of arrays
|
||||
const selectedFields: Record<string, any[]> = pickObjectKeysWithRemapping(js, cifSchemaKeys, fieldRemapping);
|
||||
return objectOfArraysToArrayOfObjects(selectedFields);
|
||||
}
|
||||
}
|
||||
|
||||
/** Same as `Table.getRows` but omits `.` and `?` fields (instead of using type defaults) */
|
||||
function getRowsFromTable<S extends Table.Schema>(table: Table<S>): Partial<Table.Row<S>>[] {
|
||||
const rows: Partial<Table.Row<S>>[] = [];
|
||||
const columns = table._columns;
|
||||
const nRows = table._rowCount;
|
||||
const Present = Column.ValueKind.Present;
|
||||
for (let iRow = 0; iRow < nRows; iRow++) {
|
||||
const row: Partial<Table.Row<S>> = {};
|
||||
for (const col of columns) {
|
||||
if (table[col].valueKind(iRow) === Present) {
|
||||
row[col as keyof S] = table[col].value(iRow);
|
||||
}
|
||||
}
|
||||
rows[iRow] = row;
|
||||
function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema, fieldRemapping: FieldRemapping): MVSAnnotationRow[] {
|
||||
const cifSchema = getCifAnnotationSchema(schema);
|
||||
const cifSchemaKeys = Object.keys(cifSchema) as (keyof typeof cifSchema)[];
|
||||
const columns: Partial<Record<keyof typeof cifSchema, any[]>> = {};
|
||||
for (const key of cifSchemaKeys) {
|
||||
let srcKey = fieldRemapping[key];
|
||||
if (srcKey === null) continue; // Ignore key
|
||||
if (srcKey === undefined) srcKey = key; // Implicit key mapping
|
||||
const columnArray = getArrayFromCifCategory(data, srcKey, cifSchema[key]); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or ''
|
||||
if (columnArray) columns[key] = columnArray;
|
||||
}
|
||||
return rows;
|
||||
if (Object.keys(columns).length === 0) return new Array(data.rowCount).fill({});
|
||||
return objectOfArraysToArrayOfObjects(columns);
|
||||
}
|
||||
|
||||
/** Load data from a specific column in a CIF category into an array. Load `.` and `?` as undefined. */
|
||||
function getArrayFromCifCategory<T>(data: CifCategory, columnName: string, columnSchema: Column.Schema): (T | undefined)[] | undefined {
|
||||
if (data.getField(columnName) === undefined) return undefined;
|
||||
|
||||
const table = toTable({ [columnName]: columnSchema }, data); // a bit dumb, I don't know how to make column directly
|
||||
const column = table[columnName];
|
||||
return getArrayFromCifColumn(column); // Avoiding `column.toArray` as it replaces . and ? fields by 0 or ''
|
||||
}
|
||||
|
||||
/** Same as `column.toArray` but reads `.` and `?` as undefined (instead of using type defaults) */
|
||||
function getArrayFromCifColumn<T>(column: Column<T>): (T | undefined)[] {
|
||||
const nRows = column.rowCount;
|
||||
const Present = Column.ValueKind.Present;
|
||||
const out: (T | undefined)[] = new Array(nRows);
|
||||
for (let iRow = 0; iRow < nRows; iRow++) {
|
||||
out[iRow] = column.valueKind(iRow) === Present ? column.value(iRow) : undefined;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
async function getFileFromSource(ctx: CustomProperty.Context, source: MVSAnnotationSource, model?: Model): Promise<MVSAnnotationFile> {
|
||||
@@ -382,3 +460,11 @@ function annotationSourceFromSpec(s: MVSAnnotationSpec): MVSAnnotationSource {
|
||||
return { kind: 'source-cif' };
|
||||
}
|
||||
}
|
||||
|
||||
/** In `array`, set value `fillValue` to all positions described by `fillRanges`. In case `array` is `null`, initialize it with length `n` prefilled with -1. */
|
||||
function fillValueOnRanges(array: IndexedElements, n: number, fillRanges: ElementRanges | undefined, fillValue: number): IndexedElements {
|
||||
if (!fillRanges || ElementRanges.count(fillRanges) === 0) return array;
|
||||
const out = array ?? Array(n).fill(-1);
|
||||
ElementRanges.foreach(fillRanges, (from, to) => out.fill(fillValue, from, to));
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -75,7 +75,7 @@ function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme,
|
||||
break;
|
||||
case 'selection':
|
||||
const substructure = substructureFromSelector(structure, item.position.params.selector);
|
||||
const p = textPropsForSelection(substructure, theme.size.size, {});
|
||||
const p = textPropsForSelection(substructure, [{}]);
|
||||
const group = serialIndexOfSubstructure(structure, substructure) ?? 0;
|
||||
if (p) builder.add(item.text, p.center[0], p.center[1], p.center[2], p.depth, p.scale, group);
|
||||
break;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { hashFnv32a } from '../../../mol-data/util';
|
||||
import { murmurHash3_128_fromBytes } from '../../../mol-data/util';
|
||||
import { StringLike } from '../../../mol-io/common/string-like';
|
||||
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
@@ -112,16 +112,23 @@ export const MVSXFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any
|
||||
* add all contained files to `plugin`'s asset manager,
|
||||
* and parse the main file in the archive as MVSJ.
|
||||
* Return parsed MVS data and `sourceUrl` for resolution of relative URIs. */
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array, mainFilePath: string = 'index.mvsj'): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext, data: Uint8Array<ArrayBuffer>, mainFilePathOrOptions?: string | { mainFilePath?: string, doNotClearAssets?: boolean }): Promise<{ mvsData: MVSData, sourceUrl: string }> {
|
||||
// TODO: on next major version, streamline mainFilePathOrOptions
|
||||
if (typeof mainFilePathOrOptions === 'string') {
|
||||
mainFilePathOrOptions = { mainFilePath: mainFilePathOrOptions };
|
||||
}
|
||||
const mainFilePath = mainFilePathOrOptions?.mainFilePath ?? 'index.mvsj';
|
||||
const doNotClearAssets = mainFilePathOrOptions?.doNotClearAssets ?? false;
|
||||
|
||||
// Ensure at most one generation of MVSX file assets exists in the asset manager.
|
||||
// Hopefully, this is a reasonable compromise to ensure MVSX files work in multi-snapshot
|
||||
// states.
|
||||
clearMVSXFileAssets(plugin);
|
||||
if (!doNotClearAssets) clearMVSXFileAssets(plugin);
|
||||
|
||||
const archiveId = `ni,fnv1a;${hashFnv32a(data)}`;
|
||||
let files: { [path: string]: Uint8Array };
|
||||
const archiveId = `ni,MurmurHash3_128;${murmurHash3_128_fromBytes(data, 42)}`;
|
||||
let files: { [path: string]: Uint8Array<ArrayBuffer> };
|
||||
try {
|
||||
files = await unzip(runtimeCtx, data) as typeof files;
|
||||
files = await unzip(runtimeCtx, data.buffer) as typeof files;
|
||||
} catch (err) {
|
||||
plugin.log.error('Invalid MVSX file');
|
||||
throw err;
|
||||
@@ -138,7 +145,7 @@ export async function loadMVSX(plugin: PluginContext, runtimeCtx: RuntimeContext
|
||||
return { mvsData, sourceUrl };
|
||||
}
|
||||
|
||||
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
|
||||
export async function loadMVSData(plugin: PluginContext, data: MVSData | StringLike | Uint8Array<ArrayBuffer>, format: 'mvsj' | 'mvsx', options?: MVSLoadOptions) {
|
||||
if (typeof data === 'string' && data.startsWith('base64')) {
|
||||
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
|
||||
}
|
||||
@@ -160,12 +167,14 @@ export async function loadMVSData(plugin: PluginContext, data: MVSData | StringL
|
||||
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
|
||||
}
|
||||
await plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array);
|
||||
const parsed = await loadMVSX(plugin, ctx, data as Uint8Array<ArrayBuffer>, { doNotClearAssets: options?.appendSnapshots });
|
||||
await loadMVS(plugin, parsed.mvsData, { sanityChecks: true, ...options, sourceUrl: parsed.sourceUrl });
|
||||
}));
|
||||
} else {
|
||||
throw new Error(`Unknown MolViewSpec format: ${format}`);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function clearMVSXFileAssets(plugin: PluginContext) {
|
||||
@@ -180,7 +189,7 @@ function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string |
|
||||
}
|
||||
|
||||
/** Return a URI referencing a file within an archive, using ARCP scheme (https://arxiv.org/pdf/1809.06935.pdf).
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,fnv1a;938511930')
|
||||
* `archiveId` corresponds to the `authority` part of URI (e.g. 'uuid,EYVwjDiZhM20PWbF1OWWvQ' or 'ni,MurmurHash3_128;e6494f6be71f34c556f3de73d306780c')
|
||||
* `path` corresponds to the path to a file within the archive */
|
||||
function arcpUri(archiveId: string, path: string): string {
|
||||
return new URL(path, `arcp://${archiveId}/`).href;
|
||||
@@ -188,7 +197,7 @@ function arcpUri(archiveId: string, path: string): string {
|
||||
|
||||
/** Add a URL asset to asset manager.
|
||||
* Skip if an asset with the same URL already exists. */
|
||||
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array, options?: { isFile?: boolean }) {
|
||||
function ensureUrlAsset(manager: AssetManager, url: string, data: Uint8Array<ArrayBuffer>, options?: { isFile?: boolean }) {
|
||||
const asset = Asset.getUrlAsset(manager, url);
|
||||
if (!manager.has(asset)) {
|
||||
const filename = url.split('/').pop() ?? 'file';
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ColorTypeLocation } from '../../../mol-geo/geometry/color-data';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { stringToWords } from '../../../mol-util/string';
|
||||
import { isMVSStructure } from './is-mvs-model-prop';
|
||||
import { ElementSet, SelectorParams, isSelectorAll } from './selector';
|
||||
import { ElementSet, SelectorParams, isSelectorAll, substructureFromSelector } from './selector';
|
||||
|
||||
|
||||
/** Special value that can be used as color with null-like semantic (i.e. "no color provided").
|
||||
@@ -70,32 +72,7 @@ export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { lay
|
||||
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
|
||||
* (the caller must ensure that any required custom properties be attached). */
|
||||
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
|
||||
const colorLayers: { color: LocationColor, elementSet: ElementSet | undefined }[] = []; // undefined elementSet means 'all'
|
||||
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from end to get top layer first, bottom layer last
|
||||
const layer = props.layers[i];
|
||||
const themeProvider = colorThemeRegistry.get(layer.theme.name);
|
||||
if (!themeProvider) {
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot find it in registry.`);
|
||||
continue;
|
||||
}
|
||||
if (themeProvider.ensureCustomProperties?.attach) {
|
||||
console.warn(`Multilayer color theme: layer "${themeProvider.name}" has ensureCustomProperties.attach method, but Multilayer color theme does not call it. If the layer does not work, make sure you call ensureCustomProperties.attach somewhere.`);
|
||||
}
|
||||
const theme = themeProvider.factory(ctx, layer.theme.params);
|
||||
switch (theme.granularity) {
|
||||
case 'uniform':
|
||||
case 'instance':
|
||||
case 'group':
|
||||
case 'groupInstance':
|
||||
case 'vertex':
|
||||
case 'vertexInstance':
|
||||
const elementSet = isSelectorAll(layer.selection) ? undefined : ElementSet.fromSelector(ctx.structure, layer.selection); // treating 'all' specially for performance reasons (it's expected to be used most often)
|
||||
colorLayers.push({ color: theme.color, elementSet });
|
||||
break;
|
||||
default:
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
|
||||
}
|
||||
};
|
||||
const { colorLayers, granularity, preferSmoothing } = makeLayers(ctx, props, colorThemeRegistry);
|
||||
|
||||
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
|
||||
for (const layer of colorLayers) {
|
||||
@@ -123,8 +100,8 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
|
||||
|
||||
return {
|
||||
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
granularity,
|
||||
preferSmoothing,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Combines colors from multiple color themes.',
|
||||
@@ -132,6 +109,119 @@ function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorT
|
||||
}
|
||||
|
||||
|
||||
const GRAN_INSTANCE = 1, GRAN_GROUP = 2, GRAN_VERTEX = 4;
|
||||
|
||||
const granularityFlagsFromName = {
|
||||
'uniform': 0,
|
||||
'instance': GRAN_INSTANCE,
|
||||
'group': GRAN_GROUP,
|
||||
'groupInstance': GRAN_GROUP | GRAN_INSTANCE,
|
||||
'vertex': GRAN_VERTEX,
|
||||
'vertexInstance': GRAN_VERTEX | GRAN_INSTANCE,
|
||||
} satisfies { [name in ColorTypeLocation]: number };
|
||||
|
||||
function granularityNameFromFlags(flags: number): ColorTypeLocation {
|
||||
if (flags & GRAN_VERTEX) return flags & GRAN_INSTANCE ? 'vertexInstance' : 'vertex';
|
||||
if (flags & GRAN_GROUP) return flags & GRAN_INSTANCE ? 'groupInstance' : 'group';
|
||||
return flags & GRAN_INSTANCE ? 'instance' : 'uniform';
|
||||
}
|
||||
|
||||
interface ColorLayer {
|
||||
/** Substructure to which the layer is applied, undefined means 'all' */
|
||||
elementSet: ElementSet | undefined,
|
||||
/** Color theme for the layer */
|
||||
color: LocationColor,
|
||||
}
|
||||
|
||||
function makeLayers(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry) {
|
||||
const colorLayers: ColorLayer[] = [];
|
||||
let granularityFlags = 0;
|
||||
let preferSmoothing = false;
|
||||
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from the end to get top layer first, bottom layer last
|
||||
const layer = props.layers[i];
|
||||
const themeProvider = colorThemeRegistry.get(layer.theme.name);
|
||||
if (!themeProvider) {
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot find it in registry.`);
|
||||
continue;
|
||||
}
|
||||
if (themeProvider.ensureCustomProperties?.attach) {
|
||||
console.warn(`Multilayer color theme: layer "${themeProvider.name}" has ensureCustomProperties.attach method, but Multilayer color theme does not call it. If the layer does not work, make sure you call ensureCustomProperties.attach somewhere.`);
|
||||
}
|
||||
const theme = themeProvider.factory(ctx, layer.theme.params);
|
||||
switch (theme.granularity) {
|
||||
case 'uniform':
|
||||
case 'instance':
|
||||
case 'group':
|
||||
case 'groupInstance':
|
||||
case 'vertex':
|
||||
case 'vertexInstance':
|
||||
let elementSet: ElementSet | undefined;
|
||||
let selectionGranularity: 'uniform' | 'instance' | 'group' | 'groupInstance';
|
||||
if (!ctx.structure) {
|
||||
elementSet = {};
|
||||
selectionGranularity = 'uniform';
|
||||
} else if (isSelectorAll(layer.selection)) {
|
||||
// Treating 'all' specially for performance reasons (it's expected to be used most often)
|
||||
elementSet = undefined;
|
||||
selectionGranularity = 'uniform';
|
||||
} else {
|
||||
const substructure = substructureFromSelector(ctx.structure, layer.selection);
|
||||
elementSet = ElementSet.fromStructure(substructure);
|
||||
selectionGranularity = getSubstructureGranularity(ctx.structure, substructure);
|
||||
}
|
||||
colorLayers.push({ elementSet, color: theme.color });
|
||||
granularityFlags |= granularityFlagsFromName[selectionGranularity];
|
||||
granularityFlags |= granularityFlagsFromName[theme.granularity];
|
||||
break;
|
||||
default:
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
|
||||
}
|
||||
if (theme.preferSmoothing) preferSmoothing = true;
|
||||
}
|
||||
return { colorLayers, granularity: granularityNameFromFlags(granularityFlags), preferSmoothing };
|
||||
}
|
||||
|
||||
|
||||
function getSubstructureGranularity(parent: Structure, substructure: Structure) {
|
||||
const parentCounts: { [instance: string]: number } = {};
|
||||
for (const unit of parent.units) {
|
||||
const instance = unit.conformation.operator.instanceId;
|
||||
parentCounts[instance] ??= 0;
|
||||
parentCounts[instance] += unit.elements.length;
|
||||
}
|
||||
|
||||
const childCounts: { [instance: string]: number } = {};
|
||||
const elementsPerInstance: { [instance: string]: { [invariantId: number]: StructureElement.Set } } = {};
|
||||
for (const unit of substructure.units) {
|
||||
const instance = unit.conformation.operator.instanceId;
|
||||
childCounts[instance] ??= 0;
|
||||
childCounts[instance] += unit.elements.length;
|
||||
(elementsPerInstance[instance] ??= {})[unit.invariantId] = unit.elements;
|
||||
}
|
||||
|
||||
const parentInstances = Object.keys(parentCounts);
|
||||
const childInstances = Object.keys(childCounts);
|
||||
const groupGranularity = !childInstances.every(inst => childCounts[inst] === parentCounts[inst]);
|
||||
let instanceGranularity: boolean;
|
||||
|
||||
if (childInstances.length === 0) {
|
||||
instanceGranularity = false;
|
||||
} else if (childInstances.length < parentInstances.length) {
|
||||
instanceGranularity = true;
|
||||
} else {
|
||||
instanceGranularity = false;
|
||||
for (let i = 1; i < childInstances.length; i++) {
|
||||
if (!deepEqual(elementsPerInstance[childInstances[0]], elementsPerInstance[childInstances[i]])) {
|
||||
instanceGranularity = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (groupGranularity) return instanceGranularity ? 'groupInstance' : 'group';
|
||||
else return instanceGranularity ? 'instance' : 'uniform';
|
||||
}
|
||||
|
||||
|
||||
/** Unique name for "Multilayer" color theme */
|
||||
export const MultilayerColorThemeName = 'mvs-multilayer';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { BaseGeometry } from '../../../mol-geo/geometry/base';
|
||||
import { Lines } from '../../../mol-geo/geometry/lines/lines';
|
||||
import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
|
||||
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
@@ -25,19 +26,24 @@ import { Structure, StructureElement, StructureSelection } from '../../../mol-mo
|
||||
import { StructureQueryHelper } from '../../../mol-plugin-state/helpers/structure-query';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ShapeRepresentation } from '../../../mol-repr/shape/representation';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { StateObject, StateTransformer } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { round } from '../../../mol-util';
|
||||
import { range } from '../../../mol-util/array';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { MarkerActions } from '../../../mol-util/marker-action';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { capitalize } from '../../../mol-util/string';
|
||||
import { rowsToExpression, rowToExpression } from '../helpers/selections';
|
||||
import { collectMVSReferences, decodeColor } from '../helpers/utils';
|
||||
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode } from '../tree/mvs/mvs-tree';
|
||||
import { collectMVSReferences, decodeColor, isDefined } from '../helpers/utils';
|
||||
import { addParamDefaults } from '../tree/generic/params-schema';
|
||||
import { treeValidationIssues } from '../tree/generic/tree-validation';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree } from '../tree/molstar/molstar-tree';
|
||||
import { MVSNode, MVSTreeSchema } from '../tree/mvs/mvs-tree';
|
||||
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
@@ -77,15 +83,35 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
const url = Asset.getUrlAsset(plugin.managers.asset, params.uri);
|
||||
const asset = await plugin.managers.asset.resolve(url, 'string').runInContext(ctx);
|
||||
const node = JSON.parse(StringLike.toString(asset.data)) as MolstarSubtree<'primitives'>;
|
||||
const validationIssues = treeValidationIssues(MVSTreeSchema, node, { anyRoot: true });
|
||||
if (validationIssues) {
|
||||
throw new Error(`Invalid primitive data from ${params.uri}:\n${validationIssues.join('\n')}`);
|
||||
}
|
||||
if (node.kind !== 'primitives') {
|
||||
throw new Error(`Expected primitives node from ${params.uri}, got ${node.kind}`);
|
||||
}
|
||||
const nodeWithDefaults: MolstarSubtree<'primitives'> = {
|
||||
...node,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, node.params || {}),
|
||||
children: node.children?.map((child: any) => {
|
||||
if (child.kind === 'primitive') {
|
||||
return {
|
||||
...child,
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitive.params, child.params || {})
|
||||
};
|
||||
}
|
||||
return child;
|
||||
})
|
||||
};
|
||||
(cache as any).asset = asset;
|
||||
return new MVSPrimitivesData({
|
||||
node,
|
||||
node: nodeWithDefaults,
|
||||
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
|
||||
structureRefs: {},
|
||||
primitives: getPrimitives(node),
|
||||
options: { ...node.params },
|
||||
primitives: getPrimitives(nodeWithDefaults),
|
||||
options: { ...nodeWithDefaults.params },
|
||||
positionCache: new Map(),
|
||||
instances: getInstances(node.params),
|
||||
instances: getInstances(nodeWithDefaults.params),
|
||||
}, { label: 'Primitive Data' });
|
||||
});
|
||||
},
|
||||
@@ -94,6 +120,16 @@ export const MVSDownloadPrimitiveData = MVSTransform({
|
||||
},
|
||||
});
|
||||
|
||||
/* Cannot use MolstarSubtree<'primitives'>> because information about type of children would be lost and cause TypeScript errors in dependent code */
|
||||
interface PrimitivesSubtree {
|
||||
kind: 'primitives',
|
||||
params: MolstarNodeParams<'primitives'>,
|
||||
children?: {
|
||||
kind: 'primitive',
|
||||
params: MolstarNodeParams<'primitive'>,
|
||||
}[],
|
||||
}
|
||||
|
||||
export type MVSInlinePrimitiveData = typeof MVSInlinePrimitiveData
|
||||
export const MVSInlinePrimitiveData = MVSTransform({
|
||||
name: 'mvs-inline-primitive-data',
|
||||
@@ -101,7 +137,10 @@ export const MVSInlinePrimitiveData = MVSTransform({
|
||||
from: [SO.Root, SO.Molecule.Structure],
|
||||
to: MVSPrimitivesData,
|
||||
params: {
|
||||
node: PD.Value<MolstarSubtree<'primitives'>>(undefined as any, { isHidden: true }),
|
||||
node: PD.Value<PrimitivesSubtree>({
|
||||
kind: 'primitives',
|
||||
params: addParamDefaults(MVSTreeSchema.nodes.primitives.params, {}),
|
||||
}, { isHidden: true }),
|
||||
},
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
@@ -124,41 +163,71 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
from: MVSPrimitivesData,
|
||||
to: SO.Shape.Provider,
|
||||
params: {
|
||||
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh')
|
||||
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh'),
|
||||
clip: PD.Value<Clip.Props | undefined>(undefined, { isHidden: true })
|
||||
}
|
||||
})({
|
||||
apply({ a, params, dependencies }) {
|
||||
const structureRefs = dependencies ? collectMVSReferences([SO.Molecule.Structure], dependencies) : {};
|
||||
const context: PrimitiveBuilderContext = { ...a.data, structureRefs };
|
||||
|
||||
const snapshotKey = { snapshotKey: { ...SnapshotKey, defaultValue: a.data.options?.snapshot_key ?? '' } };
|
||||
const markdownCommands = { markdownCommands: { ...MarkdownCommands, defaultValue: a.data.node?.custom?.molstar_markdown_commands } };
|
||||
|
||||
const label = capitalize(params.kind);
|
||||
if (params.kind === 'mesh') {
|
||||
if (!hasPrimitiveKind(a.data, 'mesh')) return StateObject.Null;
|
||||
|
||||
const customMeshParams = a.data.node.custom?.molstar_mesh_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
params: {
|
||||
...PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customMeshParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
}, { label });
|
||||
} else if (params.kind === 'labels') {
|
||||
if (!hasPrimitiveKind(a.data, 'label')) return StateObject.Null;
|
||||
|
||||
const options = a.data.options;
|
||||
const bgColor = options?.label_background_color;
|
||||
const customLabelParams = a.data.node.custom?.molstar_label_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(DefaultLabelParams, { alpha: a.data.options?.label_opacity ?? 1 }),
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLabels(data, prev?.geometry),
|
||||
params: {
|
||||
...PD.withDefaults(DefaultLabelParams, {
|
||||
alpha: a.data.options?.label_opacity ?? 1,
|
||||
attachment: options?.label_attachment ?? 'middle-center',
|
||||
tether: options?.label_show_tether ?? false,
|
||||
tetherLength: options?.label_tether_length ?? 1,
|
||||
background: isDefined(bgColor),
|
||||
backgroundColor: isDefined(bgColor) ? decodeColor(bgColor) : undefined,
|
||||
clip: params.clip,
|
||||
...customLabelParams,
|
||||
}),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, props, prev: any) => buildPrimitiveLabels(data, prev?.geometry, props),
|
||||
geometryUtils: Text.Utils,
|
||||
}, { label });
|
||||
} else if (params.kind === 'lines') {
|
||||
if (!hasPrimitiveKind(a.data, 'line')) return StateObject.Null;
|
||||
|
||||
const customLineParams = a.data.node.custom?.molstar_line_params;
|
||||
return new SO.Shape.Provider({
|
||||
label,
|
||||
data: context,
|
||||
params: PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }),
|
||||
params: {
|
||||
...PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1, clip: params.clip, ...customLineParams }),
|
||||
...snapshotKey,
|
||||
...markdownCommands,
|
||||
},
|
||||
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
|
||||
geometryUtils: Lines.Utils,
|
||||
}, { label });
|
||||
@@ -168,6 +237,51 @@ export const MVSBuildPrimitiveShape = MVSTransform({
|
||||
}
|
||||
});
|
||||
|
||||
export const MVSShapeRepresentation3D = MVSTransform({
|
||||
name: 'shape-representation-3d',
|
||||
display: '3D Representation',
|
||||
from: SO.Shape.Provider,
|
||||
to: SO.Shape.Representation3D,
|
||||
params: (a, ctx: PluginContext) => {
|
||||
return a ? a.data.params : BaseGeometry.Params;
|
||||
}
|
||||
})({
|
||||
canAutoUpdate() {
|
||||
return true;
|
||||
},
|
||||
apply({ a, params }) {
|
||||
return Task.create('Shape Representation', async ctx => {
|
||||
const props = { ...PD.getDefaultValues(a.data.params), ...params };
|
||||
const repr = ShapeRepresentation(a.data.getShape, a.data.geometryUtils);
|
||||
await repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
|
||||
const pickable = !!(params as any).snapshotKey?.trim() || !!(params as any).markdownCommands;
|
||||
if (pickable) {
|
||||
repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
|
||||
return new SO.Shape.Representation3D({ repr, sourceData: a.data }, { label: a.data.label });
|
||||
});
|
||||
},
|
||||
update({ a, b, newParams }) {
|
||||
return Task.create('Shape Representation', async ctx => {
|
||||
const props = { ...b.data.repr.props, ...newParams };
|
||||
await b.data.repr.createOrUpdate(props, a.data.data).runInContext(ctx);
|
||||
b.data.sourceData = a.data;
|
||||
|
||||
const pickable = !!(newParams as any).snapshotKey?.trim() || !!(newParams as any).markdownCommands;
|
||||
if (pickable) {
|
||||
b.data.repr.setState({ pickable, markerActions: MarkerActions.Highlighting });
|
||||
}
|
||||
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const SnapshotKey = PD.Text('', { isEssential: true, disableInteractiveUpdates: true, description: 'Activate the snapshot with the provided key when clicking on the label' });
|
||||
const MarkdownCommands = PD.Value<any>(undefined, { isHidden: true });
|
||||
|
||||
/* **************************************************** */
|
||||
|
||||
class GroupManager {
|
||||
@@ -533,8 +647,11 @@ function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Sh
|
||||
);
|
||||
}
|
||||
|
||||
function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Shape<Text> {
|
||||
const labelsBuilder = TextBuilder.create(BaseLabelProps, 1024, 1024, prev);
|
||||
function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev: Text | undefined, props: PD.Values<Text.Params>): Shape<Text> {
|
||||
const labelsBuilder = TextBuilder.create({
|
||||
...BaseLabelProps,
|
||||
...props,
|
||||
}, 1024, 1024, prev);
|
||||
const state: LabelBuilderState = { groups: new GroupManager(), labels: labelsBuilder };
|
||||
|
||||
for (const c of context.primitives) {
|
||||
|
||||
@@ -10,8 +10,6 @@ import { StaticStructureComponentTypes, createStructureComponent } from '../../.
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { MolScriptBuilder } from '../../../mol-script/language/builder';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayExtend, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { mapArrayToObject, pickObjectKeys } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -47,28 +45,27 @@ export function isSelectorAll(props: Selector): props is typeof SelectorAll {
|
||||
|
||||
|
||||
/** Data structure for fast lookup of a structure element location in a substructure */
|
||||
export type ElementSet = { [modelId: UUID]: SortedArray<ElementIndex> }
|
||||
export type ElementSet = { [unitId: number]: SortedArray<ElementIndex> }
|
||||
|
||||
export const ElementSet = {
|
||||
/** Create an `ElementSet` from a structure */
|
||||
fromStructure(structure: Structure | undefined): ElementSet {
|
||||
if (!structure) return {};
|
||||
const out: ElementSet = {};
|
||||
for (const unit of structure.units) {
|
||||
out[unit.id] = unit.elements;
|
||||
}
|
||||
return out;
|
||||
},
|
||||
/** Create an `ElementSet` from the substructure of `structure` defined by `selector` */
|
||||
fromSelector(structure: Structure | undefined, selector: Selector): ElementSet {
|
||||
if (!structure) return {};
|
||||
const arrays: { [modelId: UUID]: ElementIndex[] } = {};
|
||||
const selection = substructureFromSelector(structure, selector); // using `getAtomRangesForRow` might (might not) be faster here
|
||||
for (const unit of selection.units) {
|
||||
arrayExtend(arrays[unit.model.id] ??= [], unit.elements);
|
||||
}
|
||||
const result: { [modelId: UUID]: SortedArray<ElementIndex> } = {};
|
||||
for (const modelId in arrays) {
|
||||
const array = arrays[modelId as UUID];
|
||||
sortIfNeeded(array, (a, b) => a - b);
|
||||
result[modelId as UUID] = SortedArray.ofSortedArray(array);
|
||||
}
|
||||
return result;
|
||||
return this.fromStructure(selection);
|
||||
},
|
||||
/** Decide if the element set `set` contains structure element location `location` */
|
||||
has(set: ElementSet, location: StructureElement.Location): boolean {
|
||||
const array = set[location.unit.model.id];
|
||||
const array = set[location.unit.id];
|
||||
return array ? SortedArray.has(array, location.element) : false;
|
||||
},
|
||||
};
|
||||
|
||||
36
src/extensions/mvs/components/trajectory.ts
Normal file
36
src/extensions/mvs/components/trajectory.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { getTrajectory } from '../../../mol-plugin-state/transforms/model';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { ParamDefinition } from '../../../mol-util/param-definition';
|
||||
import { getMVSReferenceObject } from '../helpers/utils';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
export const MVSTrajectoryWithCoordinates = MVSTransform({
|
||||
name: 'trajectory-with-coordinates',
|
||||
display: { name: 'Trajectory with Coordinates', description: 'Create a trajectory from existing model and the provided coordinates.' },
|
||||
from: [PluginStateObject.Molecule.Model, PluginStateObject.Molecule.Topology],
|
||||
to: PluginStateObject.Molecule.Trajectory,
|
||||
params: {
|
||||
coordinatesRef: ParamDefinition.Text('', { isHidden: true }),
|
||||
}
|
||||
})({
|
||||
apply({ a, params, dependencies }) {
|
||||
return Task.create('Create trajectory from model/topology and coordinates', async ctx => {
|
||||
const coordinates = getMVSReferenceObject([PluginStateObject.Molecule.Coordinates], dependencies, params.coordinatesRef);
|
||||
|
||||
if (!coordinates) {
|
||||
throw new Error('Coordinates not found.');
|
||||
}
|
||||
|
||||
const trajectory = await getTrajectory(ctx, a, coordinates.data);
|
||||
const props = { label: 'Trajectory', description: `${trajectory.frameCount} model${trajectory.frameCount === 1 ? '' : 's'}` };
|
||||
return new PluginStateObject.Molecule.Trajectory(trajectory, props);
|
||||
});
|
||||
}
|
||||
});
|
||||
26
src/extensions/mvs/export.ts
Normal file
26
src/extensions/mvs/export.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Zip } from '../../mol-util/zip/zip';
|
||||
import { MVSData } from './mvs-data';
|
||||
|
||||
/**
|
||||
* Creates an MVSX zip file with from the provided data and assets
|
||||
*/
|
||||
export async function createMVSX(data: MVSData, assets: { name: string, content: string | Uint8Array<ArrayBuffer> }[]) {
|
||||
const encoder = new TextEncoder();
|
||||
const files: Record<string, Uint8Array<ArrayBuffer>> = {
|
||||
'index.mvsj': encoder.encode(JSON.stringify(data)),
|
||||
};
|
||||
for (const asset of assets) {
|
||||
files[asset.name] = typeof asset.content === 'string'
|
||||
? encoder.encode(asset.content)
|
||||
: asset.content;
|
||||
}
|
||||
|
||||
const zip = await Zip(files).run();
|
||||
return new Uint8Array(zip);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { AtomRanges } from '../atom-ranges';
|
||||
import { ElementRanges } from '../element-ranges';
|
||||
|
||||
|
||||
describe('union', () => {
|
||||
@@ -12,39 +12,39 @@ describe('union', () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const b = {
|
||||
from: [11, 37, 51, 205],
|
||||
to: [15, 39, 55, 210],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const c = {
|
||||
from: [-10, 200, 300],
|
||||
to: [-5, 202, 305],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const result = {
|
||||
from: [-10, 0, 11, 20, 37, 40, 51, 60, 80, 200, 205, 300],
|
||||
to: [-5, 10, 15, 30, 39, 50, 55, 70, 90, 202, 210, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
} as ElementRanges;
|
||||
expect(ElementRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
it('union overlapping', async () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const b = {
|
||||
from: [10, 37, 51, 84, 205],
|
||||
to: [15, 40, 55, 88, 220],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const c = {
|
||||
from: [-10, 67, 200, 300],
|
||||
to: [5, 80, 210, 305],
|
||||
} as AtomRanges;
|
||||
} as ElementRanges;
|
||||
const result = {
|
||||
from: [-10, 20, 37, 51, 60, 200, 300],
|
||||
to: [15, 30, 50, 55, 90, 220, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
} as ElementRanges;
|
||||
expect(ElementRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
674
src/extensions/mvs/helpers/animation.ts
Normal file
674
src/extensions/mvs/helpers/animation.ts
Normal file
@@ -0,0 +1,674 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import * as EasingFns from '../../../mol-math/easing';
|
||||
import { clamp, lerp } from '../../../mol-math/interpolate';
|
||||
import { EPSILON, Mat3, Mat4, Quat, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { RuntimeContext } from '../../../mol-task';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { decodeColor } from '../../../mol-util/color/utils';
|
||||
import { produce } from '../../../mol-util/produce';
|
||||
import { makeContinuousPaletteCheckpoints, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from '../components/annotation-color-theme';
|
||||
import { palettePropsFromMVSPalette } from '../load-helpers';
|
||||
import { Snapshot } from '../mvs-data';
|
||||
import { MVSAnimationEasing, MVSAnimationNode, MVSAnimationSchema } from '../tree/animation/animation-tree';
|
||||
import { Tree } from '../tree/generic/tree-schema';
|
||||
import { addDefaults } from '../tree/generic/tree-utils';
|
||||
import { MVSTree } from '../tree/mvs/mvs-tree';
|
||||
import { ColorT } from '../tree/mvs/param-types';
|
||||
|
||||
export async function generateStateTransition(ctx: RuntimeContext, snapshot: Snapshot, snapshotIndex: number, snapshotCount: number) {
|
||||
if (!snapshot.animation) return undefined;
|
||||
|
||||
const tree = addDefaults(snapshot.animation, MVSAnimationSchema);
|
||||
const transitions = tree.children?.filter(child => child.kind === 'interpolate');
|
||||
if (!transitions?.length) return undefined;
|
||||
|
||||
const duration = Math.max(
|
||||
snapshot.animation.params?.duration_ms ?? 0,
|
||||
...transitions.map(t => (t.params.start_ms ?? 0) + t.params.duration_ms)
|
||||
);
|
||||
|
||||
const frames: [tree: MVSTree, time: number][] = [];
|
||||
const dt = tree.params?.frame_time_ms ?? (1000 / 60);
|
||||
const N = Math.ceil(duration / dt);
|
||||
|
||||
const nodeMap = makeNodeMap(snapshot.root, new Map(), []);
|
||||
const cache = new Map<any, InterpolationCacheEntry>();
|
||||
|
||||
const transitionGroups = groupTranstions(transitions);
|
||||
|
||||
let prevRoot: MVSTree | undefined;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
const t = i * dt;
|
||||
const root = createSnapshot(snapshot.root, transitionGroups, t, cache, nodeMap);
|
||||
|
||||
if (root === prevRoot || (prevRoot && deepEqual(root, prevRoot))) {
|
||||
frames[frames.length - 1][1] += dt;
|
||||
} else {
|
||||
frames.push([root, dt]);
|
||||
}
|
||||
|
||||
prevRoot = root;
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Generating transition for snapshot ${snapshotIndex + 1}/${snapshotCount}`, current: i + 1, max: N });
|
||||
}
|
||||
}
|
||||
|
||||
return { tree, frametimeMs: dt, frames };
|
||||
}
|
||||
|
||||
const EasingFnMap: Record<MVSAnimationEasing, (t: number) => number> = {
|
||||
'linear': t => t,
|
||||
'bounce-in': EasingFns.bounceIn,
|
||||
'bounce-out': EasingFns.bounceOut,
|
||||
'bounce-in-out': EasingFns.bounceInOut,
|
||||
'circle-in': EasingFns.circleIn,
|
||||
'circle-out': EasingFns.circleOut,
|
||||
'circle-in-out': EasingFns.circleInOut,
|
||||
'cubic-in': EasingFns.cubicIn,
|
||||
'cubic-out': EasingFns.cubicOut,
|
||||
'cubic-in-out': EasingFns.cubicInOut,
|
||||
'exp-in': EasingFns.expIn,
|
||||
'exp-out': EasingFns.expOut,
|
||||
'exp-in-out': EasingFns.expInOut,
|
||||
'quad-in': EasingFns.quadIn,
|
||||
'quad-out': EasingFns.quadOut,
|
||||
'quad-in-out': EasingFns.quadInOut,
|
||||
'sin-in': EasingFns.sinIn,
|
||||
'sin-out': EasingFns.sinOut,
|
||||
'sin-in-out': EasingFns.sinInOut,
|
||||
};
|
||||
|
||||
interface InterpolationCacheEntry {
|
||||
paletteFn?: (value: number) => Color,
|
||||
startColor?: Color | Record<number | string, Color>,
|
||||
endColor?: Color | Record<number | string, Color>,
|
||||
rotation?: { axis: Vec3, angle: number, start: Quat, end: Quat },
|
||||
}
|
||||
|
||||
function getTransitionKey(transition: MVSAnimationNode<'interpolate'>) {
|
||||
const prop = transition.params.property;
|
||||
if (Array.isArray(prop)) {
|
||||
return `${transition.params.target_ref}:${prop.join('.')}`;
|
||||
}
|
||||
return `${transition.params.target_ref}:${prop}`;
|
||||
}
|
||||
|
||||
function groupTranstions(transitions: MVSAnimationNode<'interpolate'>[]) {
|
||||
const map = new Map<string, MVSAnimationNode<'interpolate'>[]>();
|
||||
const groups: MVSAnimationNode<'interpolate'>[][] = [];
|
||||
for (const t of transitions) {
|
||||
const key = getTransitionKey(t);
|
||||
if (!map.has(key)) {
|
||||
const group: MVSAnimationNode<'interpolate'>[] = [];
|
||||
map.set(key, group);
|
||||
groups.push(group);
|
||||
}
|
||||
map.get(key)!.push(t);
|
||||
}
|
||||
for (const group of groups) {
|
||||
group.sort((a, b) => {
|
||||
const s = (a.params.start_ms ?? 0) - (b.params.start_ms ?? 0);
|
||||
if (s !== 0) return s;
|
||||
return a.params.duration_ms - b.params.duration_ms;
|
||||
});
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
|
||||
function createSnapshot(tree: MVSTree, transitionGroups: MVSAnimationNode<'interpolate'>[][], time: number, cache: Map<any, InterpolationCacheEntry>, nodeMap: Map<string, (string | number)[]>) {
|
||||
let modified = false;
|
||||
const ret = produce(tree, (draft) => {
|
||||
for (const transitionGroup of transitionGroups) {
|
||||
|
||||
const pivot = transitionGroup[0];
|
||||
const nodePath = nodeMap.get(pivot.params.target_ref);
|
||||
if (!nodePath) continue;
|
||||
|
||||
const node = select(draft, nodePath, 0);
|
||||
const target = pivot.params.property[0] === 'custom' ? node?.custom : node?.params;
|
||||
if (!target) continue;
|
||||
|
||||
|
||||
const offset = pivot.params.property[0] === 'custom' ? 1 : 0;
|
||||
|
||||
let transition: MVSAnimationNode<'interpolate'> = pivot;
|
||||
let previous: MVSAnimationNode<'interpolate'> | undefined;
|
||||
|
||||
for (let i = transitionGroup.length - 1; i > 0; i--) {
|
||||
const current = transitionGroup[i];
|
||||
const currentStart = current.params.start_ms ?? 0;
|
||||
if (time >= currentStart) {
|
||||
transition = current;
|
||||
previous = i > 0 ? transitionGroup[i - 1] : undefined;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cache.has(transition)) {
|
||||
cache.set(transition, {});
|
||||
}
|
||||
|
||||
const cacheEntry: InterpolationCacheEntry = cache.get(transition)!;
|
||||
|
||||
const startTime: number = transition.params.start_ms ?? 0;
|
||||
const durationMs: number = transition.params.duration_ms ?? 0;
|
||||
const t = (time - startTime) / durationMs;
|
||||
|
||||
let next: any;
|
||||
if (transition.params.kind === 'transform_matrix') {
|
||||
next = processTransformMatrix(transition, target, clamp(t, 0, 1), cacheEntry, offset, previous);
|
||||
} else {
|
||||
next = processScalarLike(transition, target, t, cacheEntry, offset, previous);
|
||||
}
|
||||
|
||||
if (next === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modified = true;
|
||||
assign(target, transition.params.property, next, offset);
|
||||
}
|
||||
});
|
||||
return modified ? ret : tree;
|
||||
}
|
||||
|
||||
function applyFrequency(t: number, frequency: number, alternate: boolean) {
|
||||
let v = (t * (frequency || 1));
|
||||
if (v < 1) return v;
|
||||
|
||||
if (!alternate) {
|
||||
v = (v % 1);
|
||||
if (v === 0) return 1;
|
||||
return v;
|
||||
}
|
||||
|
||||
if (Math.abs(v - 1) < EPSILON) return 1;
|
||||
v = v % 2;
|
||||
if (v > 1) return 2 - v;
|
||||
return v;
|
||||
}
|
||||
|
||||
function getPreviousScalarEnd(previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (!previous || previous.params.kind === 'transform_matrix') return undefined;
|
||||
return previous.params.end;
|
||||
}
|
||||
|
||||
function processScalarLike(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cacheEntry: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind === 'transform_matrix') return;
|
||||
if (previous && previous.params.kind === 'transform_matrix') return;
|
||||
|
||||
const startValue = transition.params.start ?? getPreviousScalarEnd(previous) ?? select(target, transition.params.property, offset);
|
||||
if (transition.params.kind === 'color' && !cacheEntry.paletteFn) {
|
||||
cacheEntry.paletteFn = makePaletteFunction(transition);
|
||||
}
|
||||
|
||||
const endValue: any = transition.params.end;
|
||||
|
||||
if (time <= 0) return startValue;
|
||||
else if (time >= 1 - EPSILON && !transition.params.alternate_direction && transition.params.kind !== 'color') return endValue;
|
||||
|
||||
let t = clamp(time, 0, 1);
|
||||
t = applyFrequency(t, transition.params.frequency ?? 1, !!transition.params.alternate_direction);
|
||||
|
||||
const easing = EasingFnMap[transition.params.easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
t = easing(t);
|
||||
|
||||
if (transition.params.kind === 'scalar') {
|
||||
return interpolateScalars(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.discrete);
|
||||
} else if (transition.params.kind === 'vec3') {
|
||||
return interpolateVectors(startValue, endValue, t, transition.params.noise_magnitude ?? 0, !!transition.params.spherical);
|
||||
} else if (transition.params.kind === 'rotation_matrix') {
|
||||
return interpolateRotation(startValue, endValue, t, transition.params.noise_magnitude ?? 0, cacheEntry);
|
||||
} else if (transition.params.kind === 'color') {
|
||||
if (cacheEntry.paletteFn) {
|
||||
const color = cacheEntry.paletteFn(t);
|
||||
return Color.toHexStyle(color);
|
||||
}
|
||||
|
||||
const baseColors = typeof startValue === 'object' ? select(target, transition.params.property, offset) : undefined;
|
||||
return interpolateColors(startValue, endValue, t, cacheEntry, baseColors);
|
||||
}
|
||||
}
|
||||
|
||||
function getPreviousMatrixEnd(previous: MVSAnimationNode<'interpolate'> | undefined, prop: 'rotation_start' | 'translation_start' | 'scale_start') {
|
||||
if (!previous || previous.params.kind !== 'transform_matrix') return undefined;
|
||||
return previous.params[prop];
|
||||
}
|
||||
|
||||
const TransformState = {
|
||||
pivotTranslation: Mat4(),
|
||||
pivotTranslationInv: Mat4(),
|
||||
rotation: Mat4(),
|
||||
scale: Mat4(),
|
||||
translation: Mat4(),
|
||||
pivotNeg: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function processTransformMatrix(transition: MVSAnimationNode<'interpolate'>, target: any, time: number, cache: InterpolationCacheEntry, offset: number, previous: MVSAnimationNode<'interpolate'> | undefined) {
|
||||
if (transition.params.kind !== 'transform_matrix') return;
|
||||
if (previous && previous.params.kind !== 'transform_matrix') return;
|
||||
|
||||
const transform = select(target, transition.params.property, offset) ?? Mat4.identity();
|
||||
|
||||
const startRotation = transition.params.rotation_start ?? getPreviousMatrixEnd(previous, 'rotation_start') ?? Mat3.fromMat4(Mat3(), transform);
|
||||
const startTranslation = transition.params.translation_start ?? getPreviousMatrixEnd(previous, 'translation_start') ?? Mat4.getTranslation(Vec3(), transform);
|
||||
const startScale = transition.params.scale_start ?? getPreviousMatrixEnd(previous, 'scale_start') ?? Mat4.getScaling(Vec3(), transform);
|
||||
|
||||
const endRotation = transition.params.rotation_end;
|
||||
const endTranslation = transition.params.translation_end;
|
||||
const endScale = transition.params.scale_end;
|
||||
|
||||
let rotation, translation, scale;
|
||||
|
||||
if (time <= 0) {
|
||||
rotation = startRotation as Mat3;
|
||||
translation = startTranslation as Vec3;
|
||||
scale = startScale as Vec3;
|
||||
} else {
|
||||
const clampedTime = clamp(time, 0, 1);
|
||||
|
||||
let t = applyFrequency(clampedTime, transition.params.rotation_frequency ?? 1, !!transition.params.rotation_alternate_direction);
|
||||
let easing = EasingFnMap[transition.params.rotation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
rotation = interpolateRotation(startRotation as Mat3, endRotation as Mat3, easing(t), transition.params.rotation_noise_magnitude ?? 0, cache);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.translation_frequency ?? 1, !!transition.params.translation_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.translation_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
translation = interpolateVec3(startTranslation as Vec3, endTranslation as Vec3 | undefined, easing(t), transition.params.translation_noise_magnitude ?? 0, false);
|
||||
|
||||
t = applyFrequency(clampedTime, transition.params.scale_frequency ?? 1, !!transition.params.scale_alternate_direction);
|
||||
easing = EasingFnMap[transition.params.scale_easing ?? 'linear'] ?? EasingFnMap['linear'];
|
||||
scale = interpolateVec3(startScale as Vec3, endScale as Vec3 | undefined, easing(t), transition.params.scale_noise_magnitude ?? 0, false);
|
||||
}
|
||||
|
||||
const pivot = transition.params.pivot ?? Vec3.zero();
|
||||
|
||||
Mat4.fromTranslation(TransformState.translation, translation);
|
||||
Mat4.fromScaling(TransformState.scale, scale);
|
||||
Mat4.setIdentity(TransformState.rotation);
|
||||
Mat4.fromMat3(TransformState.rotation, rotation);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslation, pivot as Vec3);
|
||||
Mat4.fromTranslation(TransformState.pivotTranslationInv, Vec3.negate(TransformState.pivotNeg, pivot as Vec3));
|
||||
|
||||
// translation . pivot . rotation . scale . pivotInv
|
||||
const result = Mat4();
|
||||
Mat4.mul(result, TransformState.scale, TransformState.pivotTranslationInv);
|
||||
Mat4.mul(result, TransformState.rotation, result);
|
||||
Mat4.mul(result, TransformState.translation, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function interpolateScalars(start: number | number[], end: number | number[] | undefined, t: number, noise: number, discrete: boolean) {
|
||||
if (Array.isArray(start)) {
|
||||
const ret = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
if (!end || !Array.isArray(end)) {
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end, t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
for (let i = 0; i < start.length; i++) {
|
||||
ret[i] = interpolateScalar(start[i], end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (Array.isArray(end)) {
|
||||
const ret = Array.from<number>({ length: end.length }).fill(0.1);
|
||||
for (let i = 0; i < end.length; i++) {
|
||||
ret[i] = interpolateScalar(start, end[i], t, noise, discrete);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return interpolateScalar(start, end, t, noise, discrete);
|
||||
}
|
||||
|
||||
function interpolateScalar(start: number, end: number | undefined, t: number, noise: number, discrete: boolean) {
|
||||
let v = typeof end === 'number' ? lerp(start, end, t) : start;
|
||||
if (noise) {
|
||||
v += (Math.random() - 0.5) * noise;
|
||||
}
|
||||
if (discrete) {
|
||||
v = Math.round(v);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const InterpolateVectorsState = {
|
||||
start: Vec3(),
|
||||
end: Vec3(),
|
||||
v: Vec3(),
|
||||
};
|
||||
function interpolateVectors(start: number[], end: number[] | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
const ret: number[] = Array.from<number>({ length: start.length }).fill(0.1);
|
||||
|
||||
for (let i = 0; i < start.length; i += 3) {
|
||||
const s = Vec3.fromArray(InterpolateVectorsState.start, start, i);
|
||||
|
||||
let v: Vec3;
|
||||
if (end) {
|
||||
const e = Vec3.fromArray(InterpolateVectorsState.end, end, i);
|
||||
v = isSpherical
|
||||
? Vec3.slerp(InterpolateVectorsState.v, s, e, t)
|
||||
: Vec3.lerp(InterpolateVectorsState.v, s, e, t);
|
||||
} else {
|
||||
v = Vec3.clone(s);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
|
||||
Vec3.toArray(v, ret, i);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
const Vec3Noise = Vec3();
|
||||
function interpolateVec3(start: Vec3, end: Vec3 | undefined, t: number, noise: number, isSpherical: boolean) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
let v: Vec3;
|
||||
|
||||
if (end) {
|
||||
v = isSpherical
|
||||
? Vec3.slerp(Vec3(), start, end, t)
|
||||
: Vec3.lerp(Vec3(), start, end, t);
|
||||
} else {
|
||||
v = Vec3.clone(start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(Vec3Noise, noise);
|
||||
Vec3.add(v, v, Vec3Noise);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
const RotationState = {
|
||||
start: Quat(),
|
||||
end: Quat(),
|
||||
v: Quat(),
|
||||
noise: Quat(),
|
||||
axis: Vec3(),
|
||||
temp: Mat4(),
|
||||
};
|
||||
function interpolateRotation(start: Mat3, end: Mat3 | undefined, t: number, noise: number, cache: InterpolationCacheEntry) {
|
||||
if ((!end || start === end) && !noise) return start;
|
||||
|
||||
if (end) {
|
||||
if (!cache.rotation) {
|
||||
cache.rotation = {
|
||||
...relativeAxisAngle(start, end),
|
||||
start: Quat.fromMat3(Quat(), start),
|
||||
end: Quat.fromMat3(Quat(), end),
|
||||
};
|
||||
}
|
||||
|
||||
const { axis, angle, start: startQ, end: endQ } = cache.rotation;
|
||||
|
||||
if (angle < 1e-6) {
|
||||
// start ≈ end: make a clean spin about the detected (or default) axis
|
||||
Quat.setAxisAngle(RotationState.v, axis, t * 2 * Math.PI); // Make a full turn
|
||||
} else {
|
||||
// Normal case: stick with your existing slerp between start/end
|
||||
Quat.slerp(RotationState.v, startQ, endQ, t);
|
||||
}
|
||||
} else {
|
||||
Quat.fromMat3(RotationState.v, start);
|
||||
}
|
||||
|
||||
if (noise && t <= 1 - EPSILON) {
|
||||
Vec3.random(RotationState.axis, 1);
|
||||
Quat.setAxisAngle(RotationState.noise, RotationState.axis, 2 * Math.PI * noise * (Math.random() - 0.5));
|
||||
Quat.multiply(RotationState.v, RotationState.noise, RotationState.v);
|
||||
}
|
||||
Mat4.fromQuat(RotationState.temp, RotationState.v);
|
||||
return Mat3.fromMat4(Mat3(), RotationState.temp);
|
||||
}
|
||||
|
||||
function decodeColors(color: ColorT | Record<number | string, ColorT> | undefined, baseColors: Record<number | string, ColorT> | undefined) {
|
||||
if (color === undefined || color === null) return undefined;
|
||||
|
||||
if (typeof color === 'object') {
|
||||
const ret: Record<number | string, Color> = {};
|
||||
if (baseColors) {
|
||||
for (const key of Object.keys(baseColors)) {
|
||||
const decoded = decodeColor(baseColors[key]);
|
||||
if (decoded !== undefined) {
|
||||
ret[key] = decoded;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(color)) {
|
||||
const decoded = decodeColor(color[key]);
|
||||
if (decoded !== undefined) {
|
||||
ret[key] = decoded;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
return decodeColor(color);
|
||||
}
|
||||
|
||||
function interpolateColors(start: ColorT | Record<number, ColorT>, end: ColorT | Record<number, ColorT> | undefined, time: number, cacheEntry: InterpolationCacheEntry, baseColors: Record<number, ColorT> | undefined) {
|
||||
const t = clamp(time, 0, 1);
|
||||
|
||||
if (cacheEntry.paletteFn) {
|
||||
const c = cacheEntry.paletteFn(t);
|
||||
return Color.toHexStyle(c);
|
||||
}
|
||||
|
||||
if (cacheEntry.startColor === undefined) {
|
||||
cacheEntry.startColor = decodeColors(start, baseColors);
|
||||
}
|
||||
if (cacheEntry.endColor === undefined) {
|
||||
cacheEntry.endColor = decodeColors(end, undefined);
|
||||
}
|
||||
|
||||
const { startColor, endColor } = cacheEntry;
|
||||
|
||||
if (typeof startColor === 'object') {
|
||||
if (typeof baseColors !== 'object') {
|
||||
throw new Error('Cannot interpolate from scalar color to color mapping');
|
||||
}
|
||||
|
||||
const ret = { ...baseColors as any, ...startColor as any };
|
||||
if (typeof endColor === 'object') {
|
||||
for (const key of Object.keys(endColor)) {
|
||||
ret[key] = Color.toHexStyle(Color.interpolate(startColor[key], endColor[key], t));
|
||||
}
|
||||
} else if (typeof endColor === 'number') {
|
||||
for (const key of Object.keys(startColor)) {
|
||||
ret[key] = Color.toHexStyle(Color.interpolate(startColor[key], endColor, t));
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
if (typeof endColor === 'object') {
|
||||
throw new Error('Cannot interpolate from scalar color to color mapping');
|
||||
}
|
||||
|
||||
if (typeof endColor === 'number' && typeof startColor === 'number') {
|
||||
return Color.toHexStyle(Color.interpolate(startColor, endColor, t));
|
||||
}
|
||||
|
||||
return start;
|
||||
}
|
||||
|
||||
function select(params: any, path: string | (string | number)[], offset: number) {
|
||||
if (typeof path === 'string') {
|
||||
return params?.[path];
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
f = f[path[i]];
|
||||
}
|
||||
|
||||
return f;
|
||||
}
|
||||
|
||||
function assign(params: any, path: string | (string | number)[], value: any, offset: number) {
|
||||
if (!params) return;
|
||||
|
||||
if (typeof path === 'string') {
|
||||
params[path] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
let f = params;
|
||||
for (let i = offset; i < path.length; i++) {
|
||||
if (!f) break;
|
||||
if (i === path.length - 1) {
|
||||
f[path[i]] = value;
|
||||
} else {
|
||||
f = f[path[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeNodeMap(tree: Tree, map: Map<string, (string | number)[]>, currentPath: (string | number)[]) {
|
||||
if (tree.ref) {
|
||||
map.set(tree.ref, [...currentPath]);
|
||||
}
|
||||
|
||||
if (!tree.children) return map;
|
||||
|
||||
currentPath.push('children');
|
||||
for (let i = 0; i < tree.children.length; i++) {
|
||||
const child = tree.children[i];
|
||||
currentPath.push(i);
|
||||
makeNodeMap(child, map, currentPath);
|
||||
currentPath.pop();
|
||||
}
|
||||
currentPath.pop();
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
function makePaletteFunction(props: MVSAnimationNode<'interpolate'>): ((value: number) => Color) | undefined {
|
||||
if (props.params.kind !== 'color' || !props.params.palette) return undefined;
|
||||
|
||||
const params = palettePropsFromMVSPalette(props.params.palette);
|
||||
if (params.name === 'discrete') return makePaletteFunctionDiscrete(params.params);
|
||||
if (params.name === 'continuous') return makePaletteFunctionContinuous(params.params);
|
||||
throw new Error(`NotImplementedError: makePaletteFunction for ${(props as any).name}`);
|
||||
}
|
||||
|
||||
|
||||
function makePaletteFunctionDiscrete(props: MVSDiscretePaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
if (props.colors.length === 0) return () => defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
for (let i = props.colors.length - 1; i >= 0; i--) {
|
||||
const { color, fromValue, toValue } = props.colors[i];
|
||||
if (fromValue <= x && x <= toValue) return color;
|
||||
}
|
||||
return defaultColor;
|
||||
};
|
||||
}
|
||||
|
||||
function makePaletteFunctionContinuous(props: MVSContinuousPaletteProps): (value: number) => Color {
|
||||
const defaultColor = Color(0x0);
|
||||
const { colors, checkpoints } = makeContinuousPaletteCheckpoints(props);
|
||||
if (colors.length === 0) return () => defaultColor;
|
||||
|
||||
const underflowColor = props.setUnderflowColor ? props.underflowColor : defaultColor;
|
||||
const overflowColor = props.setOverflowColor ? props.overflowColor : defaultColor;
|
||||
|
||||
return (value: number) => {
|
||||
const x = clamp(value, 0, 1);
|
||||
const gteIdx = SortedArray.findPredecessorIndex(checkpoints, x); // Index of the first greater or equal checkpoint
|
||||
if (gteIdx === 0) {
|
||||
if (x === checkpoints[0]) return colors[0];
|
||||
else return underflowColor;
|
||||
}
|
||||
if (gteIdx === checkpoints.length) {
|
||||
return overflowColor;
|
||||
}
|
||||
const q = (x - checkpoints[gteIdx - 1]) / (checkpoints[gteIdx] - checkpoints[gteIdx - 1]);
|
||||
return Color.interpolate(colors[gteIdx - 1], colors[gteIdx], q);
|
||||
};
|
||||
}
|
||||
|
||||
const RelativeAxisAngleState = {
|
||||
Rt: Mat3(),
|
||||
R: Mat3(),
|
||||
};
|
||||
function relativeAxisAngle(start: Mat3, end: Mat3): { axis: Vec3, angle: number } {
|
||||
// R_rel = end * start^T
|
||||
const R0 = start, R1 = end;
|
||||
const Rt = Mat3.transpose(RelativeAxisAngleState.Rt, R0);
|
||||
const R = Mat3.mul(RelativeAxisAngleState.R, R1, Rt);
|
||||
|
||||
const tr = R[0] + R[4] + R[8]; // trace
|
||||
let angle = Math.acos(clamp((tr - 1) * 0.5, -1, 1)); // in [0, π]
|
||||
const axis = Vec3();
|
||||
|
||||
const eps = 1e-6;
|
||||
const sinA = Math.sin(angle);
|
||||
|
||||
if (angle < eps) {
|
||||
// Near identity: axis undefined; return any unit axis (choose something stable)
|
||||
Vec3.set(axis, 0, 0, 1);
|
||||
angle = 0.0;
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
if (Math.PI - angle > 1e-4) {
|
||||
// General case
|
||||
axis[0] = (R[5] - R[7]) / (2 * sinA); // (r32 - r23)
|
||||
axis[1] = (R[6] - R[2]) / (2 * sinA); // (r13 - r31)
|
||||
axis[2] = (R[1] - R[3]) / (2 * sinA); // (r21 - r12)
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle };
|
||||
}
|
||||
|
||||
// angle ~ π: use diagonal-based extraction for stability
|
||||
// Compute squared components then pick the largest to avoid precision loss
|
||||
const xx = Math.max(0, (R[0] + 1) * 0.5);
|
||||
const yy = Math.max(0, (R[4] + 1) * 0.5);
|
||||
const zz = Math.max(0, (R[8] + 1) * 0.5);
|
||||
|
||||
let x = Math.sqrt(xx), y = Math.sqrt(yy), z = Math.sqrt(zz);
|
||||
|
||||
if (x >= y && x >= z) {
|
||||
x = Math.max(x, 1e-8);
|
||||
y = (R[1] + R[3]) / (4 * x);
|
||||
z = (R[2] + R[6]) / (4 * x);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else if (y >= x && y >= z) {
|
||||
y = Math.max(y, 1e-8);
|
||||
x = (R[1] + R[3]) / (4 * y);
|
||||
z = (R[5] + R[7]) / (4 * y);
|
||||
Vec3.set(axis, x, y, z);
|
||||
} else {
|
||||
z = Math.max(z, 1e-8);
|
||||
x = (R[2] + R[6]) / (4 * z);
|
||||
y = (R[5] + R[7]) / (4 * z);
|
||||
Vec3.set(axis, x, y, z);
|
||||
}
|
||||
|
||||
Vec3.normalize(axis, axis);
|
||||
return { axis, angle: Math.PI };
|
||||
}
|
||||
145
src/extensions/mvs/helpers/colors.ts
Normal file
145
src/extensions/mvs/helpers/colors.ts
Normal file
@@ -0,0 +1,145 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ElementSymbolColors } from '../../../mol-theme/color/element-symbol';
|
||||
import { ResidueNameColors } from '../../../mol-theme/color/residue-name';
|
||||
import { SecondaryStructureColors as SecStrColors } from '../../../mol-theme/color/secondary-structure';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorList } from '../../../mol-util/color/color';
|
||||
import { ColorLists } from '../../../mol-util/color/lists';
|
||||
import { omitObjectKeys } from '../../../mol-util/object';
|
||||
import { ColorDictNameT, ColorListNameT } from '../tree/mvs/param-types';
|
||||
import { decodeColor } from './utils';
|
||||
|
||||
|
||||
/** Colors for amino acid groups, based on Clustal (https://www.jalview.org/help/html/colourSchemes/clustal.html) */
|
||||
const AminoGroupColors = {
|
||||
aromatic: decodeColor('#15A4A4')!,
|
||||
hydrophobic: decodeColor('#80A0F0')!,
|
||||
polar: decodeColor('#15C015')!,
|
||||
positive: decodeColor('#F01505')!,
|
||||
negative: decodeColor('#C048C0')!,
|
||||
proline: decodeColor('#C0C000')!,
|
||||
cysteine: decodeColor('#F08080')!,
|
||||
glycine: decodeColor('#F09048')!,
|
||||
};
|
||||
|
||||
/** Colors for individual amino acids, based on Clustal (https://www.jalview.org/help/html/colourSchemes/clustal.html), plus Jmol colors for nucleotides (http://jmol.sourceforge.net/jscolors/) */
|
||||
const ResiduePropertyColors = {
|
||||
...ResidueNameColors,
|
||||
HIS: AminoGroupColors.aromatic,
|
||||
TYR: AminoGroupColors.aromatic,
|
||||
ALA: AminoGroupColors.hydrophobic,
|
||||
VAL: AminoGroupColors.hydrophobic,
|
||||
LEU: AminoGroupColors.hydrophobic,
|
||||
ILE: AminoGroupColors.hydrophobic,
|
||||
MET: AminoGroupColors.hydrophobic,
|
||||
PHE: AminoGroupColors.hydrophobic,
|
||||
TRP: AminoGroupColors.hydrophobic,
|
||||
SER: AminoGroupColors.polar,
|
||||
THR: AminoGroupColors.polar,
|
||||
ASN: AminoGroupColors.polar,
|
||||
GLN: AminoGroupColors.polar,
|
||||
LYS: AminoGroupColors.positive,
|
||||
ARG: AminoGroupColors.positive,
|
||||
ASP: AminoGroupColors.negative,
|
||||
GLU: AminoGroupColors.negative,
|
||||
PRO: AminoGroupColors.proline,
|
||||
CYS: AminoGroupColors.cysteine,
|
||||
GLY: AminoGroupColors.glycine,
|
||||
};
|
||||
|
||||
/** Colors for secondary structure types, based on Jmol colors (http://jmol.sourceforge.net/jscolors/) */
|
||||
const SecondaryStructureColors = {
|
||||
// Simple categories
|
||||
helix: SecStrColors.alphaHelix,
|
||||
strand: SecStrColors.betaStrand,
|
||||
turn: SecStrColors.betaTurn,
|
||||
bend: SecStrColors.bend,
|
||||
|
||||
// DSSP categories
|
||||
H: SecStrColors.alphaHelix,
|
||||
B: SecStrColors.betaStrand,
|
||||
E: SecStrColors.betaStrand,
|
||||
G: SecStrColors.threeTenHelix,
|
||||
I: SecStrColors.piHelix,
|
||||
P: Color(0xA00000), // Polyproline II helix, Jmol has no color for it
|
||||
T: SecStrColors.betaTurn,
|
||||
S: SecStrColors.bend,
|
||||
};
|
||||
|
||||
export const MvsNamedColorDicts: Record<ColorDictNameT, Record<string, Color>> = {
|
||||
ElementSymbol: omitObjectKeys(ElementSymbolColors, ['C']), // ommitting carbon color to allow easier combination of multiple color layers
|
||||
ResidueName: ResidueNameColors,
|
||||
ResidueProperties: ResiduePropertyColors,
|
||||
SecondaryStructure: SecondaryStructureColors,
|
||||
};
|
||||
|
||||
export const MvsNamedColorLists: Record<ColorListNameT, ColorList> = {
|
||||
// Sequential single-hue
|
||||
Reds: ColorLists['reds'],
|
||||
Oranges: ColorLists['oranges'],
|
||||
Greens: ColorLists['greens'],
|
||||
Blues: ColorLists['blues'],
|
||||
Purples: ColorLists['purples'],
|
||||
Greys: ColorLists['greys'],
|
||||
|
||||
// Sequential multi-hue
|
||||
OrRd: ColorLists['orange-red'],
|
||||
BuGn: ColorLists['blue-green'],
|
||||
PuBuGn: ColorLists['purple-blue-green'],
|
||||
GnBu: ColorLists['green-blue'],
|
||||
PuBu: ColorLists['purple-blue'],
|
||||
BuPu: ColorLists['blue-purple'],
|
||||
RdPu: ColorLists['red-purple'],
|
||||
PuRd: ColorLists['purple-red'],
|
||||
YlOrRd: ColorLists['yellow-orange-red'],
|
||||
YlOrBr: ColorLists['yellow-orange-brown'],
|
||||
YlGn: ColorLists['yellow-green'],
|
||||
YlGnBu: ColorLists['yellow-green-blue'],
|
||||
|
||||
Magma: ColorLists['magma'],
|
||||
Inferno: ColorLists['inferno'],
|
||||
Plasma: ColorLists['plasma'],
|
||||
Viridis: ColorLists['viridis'],
|
||||
Cividis: ColorLists['cividis'],
|
||||
Turbo: ColorLists['turbo'],
|
||||
Warm: ColorLists['warm'],
|
||||
Cool: ColorLists['cool'],
|
||||
CubehelixDefault: ColorLists['cubehelix-default'],
|
||||
|
||||
// Cyclical
|
||||
Rainbow: ColorLists['rainbow'],
|
||||
Sinebow: ColorLists['sinebow'],
|
||||
|
||||
// Diverging
|
||||
RdBu: ColorLists['red-blue'],
|
||||
RdGy: ColorLists['red-grey'],
|
||||
PiYG: ColorLists['pink-yellow-green'],
|
||||
BrBG: ColorLists['brown-white-green'],
|
||||
PRGn: ColorLists['purple-green'],
|
||||
PuOr: ColorLists['purple-orange'],
|
||||
RdYlGn: ColorLists['red-yellow-green'],
|
||||
RdYlBu: ColorLists['red-yellow-blue'],
|
||||
Spectral: ColorLists['spectral'],
|
||||
|
||||
// Categorical
|
||||
Category10: ColorLists['category-10'],
|
||||
Observable10: ColorLists['observable-10'],
|
||||
Tableau10: ColorLists['tableau-10'],
|
||||
|
||||
Set1: ColorLists['set-1'],
|
||||
Set2: ColorLists['set-2'],
|
||||
Set3: ColorLists['set-3'],
|
||||
Pastel1: ColorLists['pastel-1'],
|
||||
Pastel2: ColorLists['pastel-2'],
|
||||
Dark2: ColorLists['dark-2'],
|
||||
Paired: ColorLists['paired'],
|
||||
Accent: ColorLists['accent'],
|
||||
|
||||
// Additional lists, not standard for visualization in general, but commonly used for structures
|
||||
Chainbow: ColorLists['turbo-no-black'],
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -9,35 +9,35 @@ import { ElementIndex } from '../../../mol-model/structure';
|
||||
import { arrayExtend, range } from '../../../mol-util/array';
|
||||
|
||||
|
||||
/** Represents a collection of disjoint atom ranges in a model.
|
||||
* The number of ranges is `AtomRanges.count(ranges)`,
|
||||
* the i-th range covers atoms `[ranges.from[i], ranges.to[i])`. */
|
||||
export interface AtomRanges {
|
||||
/** Represents a collection of disjoint elements ranges in a model (atoms, spheres, or gaussians).
|
||||
* The number of ranges is `ElementRanges.count(ranges)`,
|
||||
* the i-th range covers elements `[ranges.from[i], ranges.to[i])`. */
|
||||
export interface ElementRanges {
|
||||
from: ElementIndex[],
|
||||
to: ElementIndex[],
|
||||
}
|
||||
|
||||
export const AtomRanges = {
|
||||
/** Return the number of disjoined ranges in a `AtomRanges` object */
|
||||
count(ranges: AtomRanges): number {
|
||||
export const ElementRanges = {
|
||||
/** Return the number of disjoined ranges in a `ElementRanges` object */
|
||||
count(ranges: ElementRanges): number {
|
||||
return ranges.from.length;
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` without any atoms */
|
||||
empty(): AtomRanges {
|
||||
/** Create new `ElementRanges` without any elements */
|
||||
empty(): ElementRanges {
|
||||
return { from: [], to: [] };
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` containing a single range of atoms `[from, to)` */
|
||||
single(from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
/** Create new `ElementRanges` containing a single range of elements `[from, to)` */
|
||||
single(from: ElementIndex, to: ElementIndex): ElementRanges {
|
||||
return { from: [from], to: [to] };
|
||||
},
|
||||
|
||||
/** Add a range of atoms `[from, to)` to existing `AtomRanges` and return the modified original.
|
||||
/** Add a range of elements `[from, to)` to existing `ElementRanges` and return the modified original.
|
||||
* The added range must start after the end of the last existing range
|
||||
* (if it starts just on the next atom, these two ranges will get merged). */
|
||||
add(ranges: AtomRanges, from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
const n = AtomRanges.count(ranges);
|
||||
* (if it starts just on the next element, these two ranges will get merged). */
|
||||
add(ranges: ElementRanges, from: ElementIndex, to: ElementIndex): ElementRanges {
|
||||
const n = ElementRanges.count(ranges);
|
||||
if (n > 0) {
|
||||
const lastTo = ranges.to[n - 1];
|
||||
if (from < lastTo) throw new Error('Overlapping ranges not allowed');
|
||||
@@ -55,28 +55,30 @@ export const AtomRanges = {
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` */
|
||||
foreach(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => any) {
|
||||
const n = AtomRanges.count(ranges);
|
||||
foreach(ranges: ElementRanges, func: (from: ElementIndex, to: ElementIndex) => any) {
|
||||
const n = ElementRanges.count(ranges);
|
||||
for (let i = 0; i < n; i++) func(ranges.from[i], ranges.to[i]);
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` and return an array with results */
|
||||
map<T>(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => T): T[] {
|
||||
const n = AtomRanges.count(ranges);
|
||||
map<T>(ranges: ElementRanges, func: (from: ElementIndex, to: ElementIndex) => T): T[] {
|
||||
const n = ElementRanges.count(ranges);
|
||||
const result: T[] = new Array(n);
|
||||
for (let i = 0; i < n; i++) result[i] = func(ranges.from[i], ranges.to[i]);
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Compute the set union of multiple `AtomRanges` objects (as sets of atoms) */
|
||||
union(ranges: AtomRanges[]): AtomRanges {
|
||||
const concat = AtomRanges.empty();
|
||||
/** Compute the set union of multiple `ElementRanges` objects (as sets of elements) */
|
||||
union(ranges: (ElementRanges | undefined)[]): ElementRanges {
|
||||
const concat = ElementRanges.empty();
|
||||
for (const r of ranges) {
|
||||
arrayExtend(concat.from, r.from);
|
||||
arrayExtend(concat.to, r.to);
|
||||
if (r) {
|
||||
arrayExtend(concat.from, r.from);
|
||||
arrayExtend(concat.to, r.to);
|
||||
}
|
||||
}
|
||||
const indices = range(concat.from.length).sort((i, j) => concat.from[i] - concat.from[j]); // sort by start of range
|
||||
const result = AtomRanges.empty();
|
||||
const result = ElementRanges.empty();
|
||||
let last = -1;
|
||||
for (const i of indices) {
|
||||
const from = concat.from[i];
|
||||
@@ -94,26 +96,26 @@ export const AtomRanges = {
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Return a sorted subset of `atoms` which lie in any of `ranges` (i.e. set intersection of `atoms` and `ranges`).
|
||||
/** Return a sorted subset of `elements` which lie in any of `ranges` (i.e. set intersection of `elements` and `ranges`).
|
||||
* If `out` is provided, use it to store the result (clear any old contents).
|
||||
* If `outFirstAtomIndex` is provided, fill `outFirstAtomIndex.value` with the index of the first selected atom (if any). */
|
||||
selectAtomsInRanges(atoms: SortedArray<ElementIndex>, ranges: AtomRanges, out?: ElementIndex[], outFirstAtomIndex: { value?: number } = {}): ElementIndex[] {
|
||||
* If `outFirstElementIndex` is provided, fill `outFirstElementIndex.value` with the index of the first selected element (if any). */
|
||||
selectElementsInRanges(elements: SortedArray<ElementIndex>, ranges: ElementRanges, out?: ElementIndex[], outFirstElementIndex: { value?: number } = {}): ElementIndex[] {
|
||||
out ??= [];
|
||||
out.length = 0;
|
||||
outFirstAtomIndex.value = undefined;
|
||||
outFirstElementIndex.value = undefined;
|
||||
|
||||
const nAtoms = atoms.length;
|
||||
const nRanges = AtomRanges.count(ranges);
|
||||
if (nAtoms <= nRanges) {
|
||||
// Implementation 1 (more efficient when there are fewer atoms)
|
||||
let iRange = SortedArray.findPredecessorIndex(SortedArray.ofSortedArray(ranges.to), atoms[0] + 1);
|
||||
for (let iAtom = 0; iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
const nElements = elements.length;
|
||||
const nRanges = ElementRanges.count(ranges);
|
||||
if (nElements <= nRanges) {
|
||||
// Implementation 1 (more efficient when there are fewer elements)
|
||||
let iRange = SortedArray.findPredecessorIndex(SortedArray.ofSortedArray(ranges.to), elements[0] + 1);
|
||||
for (let iElem = 0; iElem < nElements; iElem++) {
|
||||
const a = elements[iElem];
|
||||
while (iRange < nRanges && ranges.to[iRange] <= a) iRange++;
|
||||
const qualifies = iRange < nRanges && ranges.from[iRange] <= a;
|
||||
if (qualifies) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
outFirstElementIndex.value ??= iElem;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -121,11 +123,11 @@ export const AtomRanges = {
|
||||
for (let iRange = 0; iRange < nRanges; iRange++) {
|
||||
const from = ranges.from[iRange];
|
||||
const to = ranges.to[iRange];
|
||||
for (let iAtom = SortedArray.findPredecessorIndex(atoms, from); iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
for (let iElem = SortedArray.findPredecessorIndex(elements, from); iElem < nElements; iElem++) {
|
||||
const a = elements[iElem];
|
||||
if (a < to) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
outFirstElementIndex.value ??= iElem;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -7,17 +7,42 @@
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
|
||||
import { CoarseElements } from '../../../mol-model/structure/model/properties/coarse';
|
||||
import { filterInPlace, range, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { Mapping, MultiMap, NumberMap } from './utils';
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/residues/atoms in a model by their properties */
|
||||
export interface IndicesAndSortings {
|
||||
atomic?: AtomicIndicesAndSortings,
|
||||
spheres?: CoarseIndicesAndSortings,
|
||||
gaussians?: CoarseIndicesAndSortings,
|
||||
}
|
||||
|
||||
export const IndicesAndSortings = {
|
||||
/** Get `IndicesAndSortings` for a model (use a cached value or create if not available yet) */
|
||||
get(model: Model): IndicesAndSortings {
|
||||
return model._dynamicPropertyData['indices-and-sortings'] ??= this.create(model);
|
||||
},
|
||||
|
||||
/** Create `IndicesAndSortings` for a model */
|
||||
create(model: Model): IndicesAndSortings {
|
||||
return {
|
||||
atomic: createAtomicIndicesAndSortings(model),
|
||||
spheres: createCoarseIndicesAndSortings(model.coarseHierarchy.spheres),
|
||||
gaussians: createCoarseIndicesAndSortings(model.coarseHierarchy.gaussians),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/residues/atoms in an atomic model by their properties */
|
||||
export interface AtomicIndicesAndSortings {
|
||||
chainsByLabelEntityId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByLabelAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByAuthAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
residuesSortedByLabelSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesSortedByAuthSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesSortedBySourceIndex: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesByInsCode: Mapping<ChainIndex, Mapping<string, readonly ResidueIndex[]>>,
|
||||
residuesByLabelCompId: Mapping<ChainIndex, Mapping<string, readonly ResidueIndex[]>>,
|
||||
/** Indicates if each residue is listed only once in `residuesByLabelCompId` (i.e. if each residue has only one label_comp_id) */
|
||||
@@ -26,95 +51,162 @@ export interface IndicesAndSortings {
|
||||
/** Indicates if each residue is listed only once in `residuesByAuthCompId` (i.e. if each residue has only one auth_comp_id) */
|
||||
residuesByAuthCompIdIsPure: boolean,
|
||||
atomsById: Mapping<number, ElementIndex>,
|
||||
atomsByIndex: Mapping<number, ElementIndex>,
|
||||
atomsBySourceIndex: Mapping<number, ElementIndex>,
|
||||
}
|
||||
|
||||
export const IndicesAndSortings = {
|
||||
/** Get `IndicesAndSortings` for a model (use a cached value or create if not available yet) */
|
||||
get(model: Model): IndicesAndSortings {
|
||||
return model._dynamicPropertyData['indices-and-sortings'] ??= IndicesAndSortings.create(model);
|
||||
},
|
||||
/** Create `AtomicIndicesAndSortings` for a model */
|
||||
function createAtomicIndicesAndSortings(model: Model): AtomicIndicesAndSortings | undefined {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
if (nAtoms === 0) return undefined;
|
||||
|
||||
/** Create `IndicesAndSortings` for a model */
|
||||
create(model: Model): IndicesAndSortings {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
const nChains = h.chains._rowCount;
|
||||
const { label_entity_id, label_asym_id, auth_asym_id } = h.chains;
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = h.residues;
|
||||
const { label_comp_id, auth_comp_id } = h.atoms;
|
||||
const { Present } = Column.ValueKind;
|
||||
const nChains = h.chains._rowCount;
|
||||
const { label_entity_id, label_asym_id, auth_asym_id } = h.chains;
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = h.residues;
|
||||
const { label_comp_id, auth_comp_id } = h.atoms;
|
||||
const { Present } = Column.ValueKind;
|
||||
|
||||
const chainsByLabelEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByLabelAsymId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAuthAsymId = new MultiMap<string, ChainIndex>();
|
||||
const residuesSortedByLabelSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedByAuthSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesByInsCode = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
const residuesByLabelCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByLabelCompIdIsPure = true;
|
||||
const residuesByAuthCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByAuthCompIdIsPure = true;
|
||||
const atomsById = new NumberMap<number, ElementIndex>(nAtoms + 1);
|
||||
const atomsByIndex = new NumberMap<number, ElementIndex>(nAtoms);
|
||||
const chainsByLabelEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByLabelAsymId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAuthAsymId = new MultiMap<string, ChainIndex>();
|
||||
const residuesSortedByLabelSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedByAuthSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedBySourceIndex = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesByInsCode = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
const residuesByLabelCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByLabelCompIdIsPure = true;
|
||||
const residuesByAuthCompId = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
let residuesByAuthCompIdIsPure = true;
|
||||
const atomsById = new NumberMap<number, ElementIndex>(nAtoms + 1);
|
||||
const atomsBySourceIndex = new NumberMap<number, ElementIndex>(nAtoms);
|
||||
|
||||
const _labelCompIdSet = new Set<string>();
|
||||
const _authCompIdSet = new Set<string>();
|
||||
const _labelCompIdSet = new Set<string>();
|
||||
const _authCompIdSet = new Set<string>();
|
||||
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
chainsByLabelEntityId.add(label_entity_id.value(iChain), iChain);
|
||||
chainsByLabelAsymId.add(label_asym_id.value(iChain), iChain);
|
||||
chainsByAuthAsymId.add(auth_asym_id.value(iChain), iChain);
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
chainsByLabelEntityId.add(label_entity_id.value(iChain), iChain);
|
||||
chainsByLabelAsymId.add(label_asym_id.value(iChain), iChain);
|
||||
chainsByAuthAsymId.add(auth_asym_id.value(iChain), iChain);
|
||||
|
||||
const iResFrom = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain]];
|
||||
const iResTo = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
const iResFrom = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain]];
|
||||
const iResTo = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
|
||||
const residuesWithLabelSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => label_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByLabelSeqId.set(iChain, Sorting.create(residuesWithLabelSeqId, label_seq_id.value));
|
||||
const residuesWithLabelSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => label_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByLabelSeqId.set(iChain, Sorting.create(residuesWithLabelSeqId, label_seq_id.value));
|
||||
|
||||
const residuesWithAuthSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => auth_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByAuthSeqId.set(iChain, Sorting.create(residuesWithAuthSeqId, auth_seq_id.value));
|
||||
const residuesWithAuthSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => auth_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByAuthSeqId.set(iChain, Sorting.create(residuesWithAuthSeqId, auth_seq_id.value));
|
||||
|
||||
const residuesHereByInsCode = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByLabelCompId = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByAuthCompId = new MultiMap<string, ResidueIndex>();
|
||||
for (let iRes = iResFrom; iRes < iResTo; iRes++) {
|
||||
if (pdbx_PDB_ins_code.valueKind(iRes) === Present) {
|
||||
residuesHereByInsCode.add(pdbx_PDB_ins_code.value(iRes), iRes);
|
||||
}
|
||||
const iAtomFrom = h.residueAtomSegments.offsets[iRes];
|
||||
const iAtomTo = h.residueAtomSegments.offsets[iRes + 1];
|
||||
for (let iAtom = iAtomFrom; iAtom < iAtomTo; iAtom++) {
|
||||
_labelCompIdSet.add(label_comp_id.value(iAtom));
|
||||
_authCompIdSet.add(auth_comp_id.value(iAtom));
|
||||
}
|
||||
if (_labelCompIdSet.size > 1) residuesByLabelCompIdIsPure = false;
|
||||
if (_authCompIdSet.size > 1) residuesByAuthCompIdIsPure = false;
|
||||
for (const labelCompId of _labelCompIdSet) residuesHereByLabelCompId.add(labelCompId, iRes);
|
||||
for (const authCompId of _authCompIdSet) residuesHereByAuthCompId.add(authCompId, iRes);
|
||||
_labelCompIdSet.clear();
|
||||
_authCompIdSet.clear();
|
||||
const residuesWithSourceIndex = range(iResFrom, iResTo) as ResidueIndex[];
|
||||
residuesSortedBySourceIndex.set(iChain, Sorting.create(residuesWithSourceIndex, h.residueSourceIndex.value));
|
||||
|
||||
const residuesHereByInsCode = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByLabelCompId = new MultiMap<string, ResidueIndex>();
|
||||
const residuesHereByAuthCompId = new MultiMap<string, ResidueIndex>();
|
||||
for (let iRes = iResFrom; iRes < iResTo; iRes++) {
|
||||
if (pdbx_PDB_ins_code.valueKind(iRes) === Present) {
|
||||
residuesHereByInsCode.add(pdbx_PDB_ins_code.value(iRes), iRes);
|
||||
}
|
||||
residuesByInsCode.set(iChain, residuesHereByInsCode);
|
||||
residuesByLabelCompId.set(iChain, residuesHereByLabelCompId);
|
||||
residuesByAuthCompId.set(iChain, residuesHereByAuthCompId);
|
||||
const iAtomFrom = h.residueAtomSegments.offsets[iRes];
|
||||
const iAtomTo = h.residueAtomSegments.offsets[iRes + 1];
|
||||
for (let iAtom = iAtomFrom; iAtom < iAtomTo; iAtom++) {
|
||||
_labelCompIdSet.add(label_comp_id.value(iAtom));
|
||||
_authCompIdSet.add(auth_comp_id.value(iAtom));
|
||||
}
|
||||
if (_labelCompIdSet.size > 1) residuesByLabelCompIdIsPure = false;
|
||||
if (_authCompIdSet.size > 1) residuesByAuthCompIdIsPure = false;
|
||||
for (const labelCompId of _labelCompIdSet) residuesHereByLabelCompId.add(labelCompId, iRes);
|
||||
for (const authCompId of _authCompIdSet) residuesHereByAuthCompId.add(authCompId, iRes);
|
||||
_labelCompIdSet.clear();
|
||||
_authCompIdSet.clear();
|
||||
}
|
||||
residuesByInsCode.set(iChain, residuesHereByInsCode);
|
||||
residuesByLabelCompId.set(iChain, residuesHereByLabelCompId);
|
||||
residuesByAuthCompId.set(iChain, residuesHereByAuthCompId);
|
||||
}
|
||||
|
||||
const atomId = model.atomicConformation.atomId.value;
|
||||
const atomIndex = h.atomSourceIndex.value;
|
||||
for (let iAtom = 0 as ElementIndex; iAtom < nAtoms; iAtom++) {
|
||||
atomsById.set(atomId(iAtom), iAtom);
|
||||
atomsByIndex.set(atomIndex(iAtom), iAtom);
|
||||
}
|
||||
const atomId = model.atomicConformation.atomId.value;
|
||||
const atomIndex = h.atomSourceIndex.value;
|
||||
for (let iAtom = 0 as ElementIndex; iAtom < nAtoms; iAtom++) {
|
||||
atomsById.set(atomId(iAtom), iAtom);
|
||||
atomsBySourceIndex.set(atomIndex(iAtom), iAtom);
|
||||
}
|
||||
|
||||
return {
|
||||
chainsByLabelEntityId, chainsByLabelAsymId, chainsByAuthAsymId,
|
||||
residuesSortedByLabelSeqId, residuesSortedByAuthSeqId, residuesByInsCode,
|
||||
residuesByLabelCompId, residuesByLabelCompIdIsPure, residuesByAuthCompId, residuesByAuthCompIdIsPure,
|
||||
atomsById, atomsByIndex,
|
||||
};
|
||||
return {
|
||||
chainsByLabelEntityId, chainsByLabelAsymId, chainsByAuthAsymId,
|
||||
residuesSortedByLabelSeqId, residuesSortedByAuthSeqId, residuesSortedBySourceIndex, residuesByInsCode,
|
||||
residuesByLabelCompId, residuesByLabelCompIdIsPure, residuesByAuthCompId, residuesByAuthCompIdIsPure,
|
||||
atomsById, atomsBySourceIndex,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/elements in a coarse model by their properties */
|
||||
export interface CoarseIndicesAndSortings {
|
||||
/** Coarse equivalent to `model.atomicHierarchy.chains` */
|
||||
chains: {
|
||||
/** Number of chains */
|
||||
count: number,
|
||||
/** Maps chain index to `label_entity_id` value */
|
||||
label_entity_id: string[],
|
||||
/** Maps chain index to `label_asym_id` value */
|
||||
label_asym_id: string[],
|
||||
},
|
||||
};
|
||||
chainsByEntityId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
/** Coarse elements (per chain) sorted by `seq_id_begin`.
|
||||
* This is used to get the range of elements which may overlap with a certain seq_id interval.
|
||||
*
|
||||
* (Filtering coarse elements by seq_id range is an interval search problem, so the worst-case-efficient solution would be to use a data structure optimized for that.
|
||||
* But that would be overkill if we expect that in most cases the coarse elements cover non-overlapping seq_id ranges.
|
||||
* So the current solution should be sufficient (fast for non-overlapping elements, while still correct if there are overlaps).) */
|
||||
elementsSortedBySeqIdBegin: Mapping<ChainIndex,
|
||||
Sorting<ElementIndex, number> & {
|
||||
/** Non-decreasing upper bound for `seq_id_end` values of elements as listed in `keys` (`seq_id_end.value(keys[i]) <= endUpperBounds[i]`) */
|
||||
endUpperBounds: SortedArray
|
||||
}>,
|
||||
}
|
||||
|
||||
/** Create `CoarseIndicesAndSortings` for a coarse elements hierarchy */
|
||||
function createCoarseIndicesAndSortings(coarseElements: CoarseElements): CoarseIndicesAndSortings | undefined {
|
||||
if (coarseElements.count === 0) return undefined;
|
||||
const { entity_id, asym_id, seq_id_begin, seq_id_end, chainElementSegments } = coarseElements;
|
||||
const { Present } = Column.ValueKind;
|
||||
const nChains = Math.max(chainElementSegments.count, 0); // chainElementSegments.count is -1 when there are no coarse elements
|
||||
|
||||
const chainsByEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAsymId = new MultiMap<string, ChainIndex>();
|
||||
const elementsSortedBySeqIdBegin = new Map<ChainIndex, Sorting<ElementIndex, number> & { endUpperBounds: SortedArray }>();
|
||||
const chains = {
|
||||
count: nChains,
|
||||
label_entity_id: new Array<string>(nChains),
|
||||
label_asym_id: new Array<string>(nChains),
|
||||
};
|
||||
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
const iElemFrom = chainElementSegments.offsets[iChain];
|
||||
const iElemTo = chainElementSegments.offsets[iChain + 1];
|
||||
const entityId = entity_id.value(iElemFrom);
|
||||
const asymId = asym_id.value(iElemFrom);
|
||||
chains.label_entity_id[iChain] = entityId;
|
||||
chains.label_asym_id[iChain] = asymId;
|
||||
chainsByEntityId.add(entityId, iChain);
|
||||
chainsByAsymId.add(asymId, iChain);
|
||||
|
||||
const elementsWithSeqIds = filterInPlace(range(iElemFrom, iElemTo) as ElementIndex[], iElem => seq_id_begin.valueKind(iElem) === Present && seq_id_end.valueKind(iElem) === Present);
|
||||
const sorting = Sorting.create(elementsWithSeqIds, seq_id_begin.value);
|
||||
const endBounds = sorting.keys.map(seq_id_end.value);
|
||||
// Ensure non-decreasing endBounds:
|
||||
for (let i = 1; i < endBounds.length; i++) {
|
||||
if (endBounds[i - 1] > endBounds[i]) {
|
||||
endBounds[i] = endBounds[i - 1];
|
||||
}
|
||||
}
|
||||
elementsSortedBySeqIdBegin.set(iChain, { ...sorting, endUpperBounds: SortedArray.ofSortedArray(endBounds) });
|
||||
}
|
||||
|
||||
return { chains, chainsByEntityId, chainsByAsymId, elementsSortedBySeqIdBegin };
|
||||
}
|
||||
|
||||
|
||||
/** Represents a set of things (keys) of type `K`, sorted by some property (value) of type `V` */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -7,13 +7,14 @@
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ElementIndex, Model, Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { ElementIndex, Model, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
import { getPhysicalRadius } from '../../../mol-theme/size/physical';
|
||||
import { arrayExtend } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { ElementRanges } from './element-ranges';
|
||||
import { IndicesAndSortings } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { getAtomRangesForRows } from './selections';
|
||||
import { getAtomRangesForRows, getGaussianRangesForRows, getSphereRangesForRows } from './selections';
|
||||
import { isDefined } from './utils';
|
||||
|
||||
|
||||
/** Properties describing position, size, etc. of a text in 3D */
|
||||
@@ -24,47 +25,86 @@ export interface TextProps {
|
||||
depth: number,
|
||||
/** Relative text size */
|
||||
scale: number,
|
||||
/** Index of the first atom within structure, to which this text is bound (for coloring and similar purposes) */
|
||||
/** Index of the first element within structure, to which this text is bound (for coloring and similar purposes) */
|
||||
group: number,
|
||||
}
|
||||
|
||||
const tmpVec = Vec3();
|
||||
const tmpArray: number[] = [];
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
const outAtoms: ElementIndex[] = [];
|
||||
const outFirstAtomIndex: { value?: number } = {};
|
||||
const outElements: ElementIndex[] = [];
|
||||
const outFirstElementIndex: { value?: number } = {};
|
||||
|
||||
/** Helper for caching element ranges qualifying to a group of annotation rows, per `Unit`. */
|
||||
class ElementRangesCache {
|
||||
private readonly cache: { [key: string]: ElementRanges } = {};
|
||||
private readonly hasOperators: boolean;
|
||||
|
||||
constructor(private readonly rows: MVSAnnotationRow[]) {
|
||||
this.hasOperators = rows.some(row => isDefined(row.instance_id));
|
||||
}
|
||||
|
||||
get(unit: Unit): ElementRanges {
|
||||
const instanceId = unit.conformation.operator.instanceId;
|
||||
const key = `${unit.model.id}:${unit.kind}:${this.hasOperators ? instanceId : '*'}`;
|
||||
return this.cache[key] ??= this.compute(unit);
|
||||
}
|
||||
private compute(unit: Unit): ElementRanges {
|
||||
const instanceId = unit.conformation.operator.instanceId;
|
||||
const indices = IndicesAndSortings.get(unit.model);
|
||||
switch (unit.kind) {
|
||||
case Unit.Kind.Atomic:
|
||||
return getAtomRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
case Unit.Kind.Spheres:
|
||||
return getSphereRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
case Unit.Kind.Gaussians:
|
||||
return getGaussianRangesForRows(this.rows, unit.model, instanceId, indices);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Approximate number of heavy atoms per protein residue (I got 7.55 from 2e2n) */
|
||||
const AVG_ATOMS_PER_RESIDUE = 8;
|
||||
|
||||
/** Return `TextProps` (position, size, etc.) for a text that is to be bound to a substructure of `structure` defined by union of `rows`.
|
||||
* Derives `center` and `depth` from the boundary sphere of the substructure, `scale` from the number of heavy atoms in the substructure. */
|
||||
export function textPropsForSelection(structure: Structure, sizeFunction: (location: StructureElement.Location) => number, rows: MVSAnnotationRow | MVSAnnotationRow[], onlyInModel?: Model): TextProps | undefined {
|
||||
export function textPropsForSelection(structure: Structure, rows: MVSAnnotationRow[], onlyInModel?: Model): TextProps | undefined {
|
||||
const loc = StructureElement.Location.create(structure);
|
||||
const { units } = structure;
|
||||
const { type_symbol } = StructureProperties.atom;
|
||||
tmpArray.length = 0;
|
||||
let includedAtoms = 0;
|
||||
let includedElements = 0;
|
||||
let includedHeavyAtoms = 0;
|
||||
let group: number | undefined = undefined;
|
||||
let atomSize: number | undefined = undefined;
|
||||
const rangesByModel: { [modelId: UUID]: AtomRanges } = {};
|
||||
/** Used for `depth` in case the selection has only 1 element (hence bounding sphere radius is 0) */
|
||||
let singularRadius: number | undefined = undefined;
|
||||
const elementRangesCache = new ElementRangesCache(rows);
|
||||
for (let iUnit = 0, nUnits = units.length; iUnit < nUnits; iUnit++) {
|
||||
const unit = units[iUnit];
|
||||
if (onlyInModel && unit.model.id !== onlyInModel.id) continue;
|
||||
const ranges = rangesByModel[unit.model.id] ??= getAtomRangesForRows(unit.model, rows, IndicesAndSortings.get(unit.model));
|
||||
const coarseElements = unit.kind === Unit.Kind.Spheres ? unit.model.coarseHierarchy.spheres : unit.kind === Unit.Kind.Gaussians ? unit.model.coarseHierarchy.gaussians : undefined;
|
||||
const ranges = elementRangesCache.get(unit);
|
||||
ElementRanges.selectElementsInRanges(unit.elements, ranges, outElements, outFirstElementIndex);
|
||||
loc.unit = unit;
|
||||
AtomRanges.selectAtomsInRanges(unit.elements, ranges, outAtoms, outFirstAtomIndex);
|
||||
for (const atom of outAtoms) {
|
||||
loc.element = atom;
|
||||
unit.conformation.position(atom, tmpVec);
|
||||
arrayExtend(tmpArray, tmpVec);
|
||||
group ??= structure.serialMapping.cumulativeUnitElementCount[iUnit] + outFirstAtomIndex.value!;
|
||||
atomSize ??= sizeFunction(loc);
|
||||
includedAtoms++;
|
||||
if (type_symbol(loc) !== 'H') includedHeavyAtoms++;
|
||||
for (const iElem of outElements) {
|
||||
loc.element = iElem;
|
||||
arrayExtend(tmpArray, unit.conformation.position(iElem, tmpVec));
|
||||
group ??= structure.serialMapping.cumulativeUnitElementCount[iUnit] + outFirstElementIndex.value!;
|
||||
singularRadius ??= getPhysicalRadius(unit, iElem) * 1.2;
|
||||
if (coarseElements) {
|
||||
// coarse
|
||||
const nResidues = coarseElements.seq_id_end.value(iElem) - coarseElements.seq_id_begin.value(iElem) + 1;
|
||||
includedHeavyAtoms += nResidues * AVG_ATOMS_PER_RESIDUE;
|
||||
} else {
|
||||
// atomic
|
||||
if (type_symbol(loc) !== 'H') includedHeavyAtoms++;
|
||||
}
|
||||
includedElements++;
|
||||
}
|
||||
}
|
||||
if (includedAtoms > 0) {
|
||||
const { center, radius } = (includedAtoms > 1) ? boundarySphere(tmpArray) : { center: Vec3.fromArray(Vec3(), tmpArray, 0), radius: 1.1 * atomSize! };
|
||||
const scale = (includedHeavyAtoms || includedAtoms) ** (1 / 3);
|
||||
if (includedElements > 0) {
|
||||
const { center, radius } = (includedElements > 1) ? boundarySphere(tmpArray) : { center: Vec3.fromArray(Vec3(), tmpArray, 0), radius: singularRadius! };
|
||||
const scale = (includedHeavyAtoms || includedElements) ** (1 / 3);
|
||||
return { center, depth: radius, scale, group: group! };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -7,29 +7,44 @@
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
|
||||
/** Similar to `PD.Numeric` but allows leaving empty field in UI (treated as `undefined`) */
|
||||
export function MaybeIntegerParamDefinition(defaultValue?: number, info?: PD.Info): PD.Base<number | undefined> {
|
||||
return PD.Converted<number | undefined, PD.Text>(stringifyMaybeInt, parseMaybeInt, PD.Text(stringifyMaybeInt(defaultValue), info));
|
||||
/** Similar to `PD.Numeric` but allows leaving empty field in UI (treated as `undefined`), and can only have integer values */
|
||||
export function MaybeIntegerParamDefinition(info?: PD.Info & { placeholder?: string }): PD.Converted<number | null, string> {
|
||||
const defaultValue = null; // the default must be null, otherwise real nulls would be replaced by the default
|
||||
return PD.Converted(stringifyMaybeInt, parseMaybeInt, PD.Text(stringifyMaybeInt(defaultValue), { ...info, disableInteractiveUpdates: true, placeholder: info?.placeholder ?? 'null' }));
|
||||
}
|
||||
/** The magic with negative zero looks crazy, but it's needed if we want to be able to write negative numbers, LOL. Please help if you know a better solution. */
|
||||
function parseMaybeInt(input: string): number | undefined {
|
||||
if (input.trim() === '-') return -0;
|
||||
function parseMaybeInt(input: string): number | null {
|
||||
const num = parseInt(input);
|
||||
return isNaN(num) ? undefined : num;
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
function stringifyMaybeInt(num: number | undefined): string {
|
||||
if (num === undefined) return '';
|
||||
if (Object.is(num, -0)) return '-';
|
||||
function stringifyMaybeInt(num: number | null): string {
|
||||
if (num === null) return '';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
|
||||
/** Similar to `PD.Numeric` but allows leaving empty field in UI (treated as `undefined`) */
|
||||
export function MaybeFloatParamDefinition(info?: PD.Info & { placeholder?: string }): PD.Converted<number | null, string> {
|
||||
const defaultValue = null; // the default must be null, otherwise real nulls would be replaced by the default
|
||||
return PD.Converted(stringifyMaybeFloat, parseMaybeFloat, PD.Text(stringifyMaybeFloat(defaultValue), { ...info, disableInteractiveUpdates: true, placeholder: info?.placeholder ?? 'null' }));
|
||||
}
|
||||
function parseMaybeFloat(input: string): number | null {
|
||||
const num = parseFloat(input);
|
||||
return isNaN(num) ? null : num;
|
||||
}
|
||||
function stringifyMaybeFloat(num: number | null): string {
|
||||
if (num === null) return '';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
|
||||
/** Similar to `PD.Text` but leaving empty field in UI is treated as `undefined` */
|
||||
export function MaybeStringParamDefinition(defaultValue?: string, info?: PD.Info): PD.Base<string | undefined> {
|
||||
return PD.Converted<string | undefined, PD.Text>(stringifyMaybeString, parseMaybeString, PD.Text(stringifyMaybeString(defaultValue), info));
|
||||
export function MaybeStringParamDefinition(info?: PD.Info & { placeholder?: string }): PD.Converted<string | null, string> {
|
||||
const defaultValue = null; // the default must be null, otherwise real nulls would be replaced by the default
|
||||
return PD.Converted(stringifyMaybeString, parseMaybeString, PD.Text(stringifyMaybeString(defaultValue), { ...info, placeholder: info?.placeholder ?? 'null' }));
|
||||
}
|
||||
function parseMaybeString(input: string): string | undefined {
|
||||
return input === '' ? undefined : input;
|
||||
function parseMaybeString(input: string): string | null {
|
||||
return input === '' ? null : input;
|
||||
}
|
||||
function stringifyMaybeString(str: string | undefined): string {
|
||||
return str === undefined ? '' : str;
|
||||
function stringifyMaybeString(str: string | null): string {
|
||||
return str === null ? '' : str;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ const AllAtomicCifAnnotationSchema = {
|
||||
end_auth_seq_id: int,
|
||||
label_comp_id: str,
|
||||
auth_comp_id: str,
|
||||
// residue_index: int, // 0-based residue index in the source file // TODO this is defined in Python builder but not supported by Molstar yet
|
||||
/** 0-based residue index in the source file */
|
||||
residue_index: int,
|
||||
|
||||
/** Atom name like 'CA', 'N', 'O'... */
|
||||
label_atom_id: str,
|
||||
@@ -77,6 +78,9 @@ const AllAtomicCifAnnotationSchema = {
|
||||
atom_id: int,
|
||||
/** 0-based index of the atom in the source data */
|
||||
atom_index: int,
|
||||
/** Instance identifier to distinguish instances of the same chain created by applying different symmetry operators,
|
||||
* like 'ASM-X0-1' for assemblies or '1_555' for crystals */
|
||||
instance_id: str,
|
||||
} satisfies Table.Schema;
|
||||
|
||||
/** Allowed fields (i.e. CIF columns or JSON keys) for each annotation schema
|
||||
@@ -86,11 +90,11 @@ const FieldsForSchemas = {
|
||||
entity: ['group_id', 'label_entity_id'],
|
||||
chain: ['group_id', 'label_entity_id', 'label_asym_id'],
|
||||
auth_chain: ['group_id', 'auth_asym_id'],
|
||||
residue: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id'],
|
||||
auth_residue: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code'],
|
||||
residue: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'residue_index'],
|
||||
auth_residue: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'residue_index'],
|
||||
residue_range: ['group_id', 'label_entity_id', 'label_asym_id', 'beg_label_seq_id', 'end_label_seq_id'],
|
||||
auth_residue_range: ['group_id', 'auth_asym_id', 'beg_auth_seq_id', 'end_auth_seq_id'],
|
||||
atom: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'label_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
auth_atom: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'auth_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
atom: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'residue_index', 'label_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
auth_atom: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'residue_index', 'auth_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
all_atomic: Object.keys(AllAtomicCifAnnotationSchema) as (keyof typeof AllAtomicCifAnnotationSchema)[],
|
||||
} satisfies { [schema in MVSAnnotationSchema]: (keyof typeof AllAtomicCifAnnotationSchema)[] };
|
||||
|
||||
@@ -6,11 +6,13 @@
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex, StructureElement } from '../../../mol-model/structure';
|
||||
import { CoarseElements } from '../../../mol-model/structure/model/properties/coarse';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { arrayExtend, filterInPlace, range } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { IndicesAndSortings, Sorting } from './indexing';
|
||||
import { arrayExtend, filterInPlace, range, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { ElementRanges } from './element-ranges';
|
||||
import { AtomicIndicesAndSortings, CoarseIndicesAndSortings, IndicesAndSortings, Sorting } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { isAnyDefined, isDefined } from './utils';
|
||||
|
||||
@@ -18,69 +20,72 @@ import { isAnyDefined, isDefined } from './utils';
|
||||
const EmptyArray: readonly any[] = [];
|
||||
|
||||
|
||||
// ATOMIC SELECTIONS
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getAtomRangesForRow(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): AtomRanges {
|
||||
export function getAtomRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.atomic) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
|
||||
const atomicIndices = indices.atomic;
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
if (nAtoms === 0) return undefined;
|
||||
|
||||
const hasAtomIds = isAnyDefined(row.atom_id, row.atom_index);
|
||||
const hasAtomFilter = isAnyDefined(row.label_atom_id, row.auth_atom_id, row.type_symbol)
|
||||
|| isDefined(row.label_comp_id) && !indices.residuesByLabelCompIdIsPure
|
||||
|| isDefined(row.auth_comp_id) && !indices.residuesByAuthCompIdIsPure;
|
||||
|| isDefined(row.label_comp_id) && !atomicIndices.residuesByLabelCompIdIsPure
|
||||
|| isDefined(row.auth_comp_id) && !atomicIndices.residuesByAuthCompIdIsPure;
|
||||
const hasResidueFilter = isAnyDefined(row.label_seq_id, row.auth_seq_id, row.pdbx_PDB_ins_code,
|
||||
row.beg_label_seq_id, row.end_label_seq_id, row.beg_auth_seq_id, row.end_auth_seq_id,
|
||||
row.label_comp_id, row.auth_comp_id);
|
||||
row.label_comp_id, row.auth_comp_id, row.residue_index);
|
||||
const hasChainFilter = isAnyDefined(row.label_asym_id, row.auth_asym_id, row.label_entity_id);
|
||||
|
||||
if (hasAtomIds) {
|
||||
const theAtom = getTheAtomForRow(model, row, indices);
|
||||
return theAtom !== undefined ? AtomRanges.single(theAtom, theAtom + 1 as ElementIndex) : AtomRanges.empty();
|
||||
const theAtom = getTheAtomForRow(model, row, atomicIndices);
|
||||
return theAtom !== undefined ? ElementRanges.single(theAtom, theAtom + 1 as ElementIndex) : undefined;
|
||||
}
|
||||
|
||||
if (!hasChainFilter && !hasResidueFilter && !hasAtomFilter) {
|
||||
return AtomRanges.single(0 as ElementIndex, nAtoms as ElementIndex);
|
||||
return ElementRanges.single(0 as ElementIndex, nAtoms as ElementIndex);
|
||||
}
|
||||
|
||||
const qualifyingChains = getQualifyingChains(model, row, indices);
|
||||
const qualifyingChains = getQualifyingChains(model, row, atomicIndices);
|
||||
if (!hasResidueFilter && !hasAtomFilter) {
|
||||
const chainOffsets = h.chainAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iChain of qualifyingChains) {
|
||||
AtomRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
ElementRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingResidues = getQualifyingResidues(model, row, indices, qualifyingChains);
|
||||
const qualifyingResidues = getQualifyingResidues(model, row, atomicIndices, qualifyingChains);
|
||||
if (!hasAtomFilter) {
|
||||
const residueOffsets = h.residueAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iRes of qualifyingResidues) {
|
||||
AtomRanges.add(ranges, residueOffsets[iRes], residueOffsets[iRes + 1]);
|
||||
ElementRanges.add(ranges, residueOffsets[iRes], residueOffsets[iRes + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingAtoms = getQualifyingAtoms(model, row, indices, qualifyingResidues);
|
||||
const ranges = AtomRanges.empty();
|
||||
const qualifyingAtoms = getQualifyingAtoms(model, row, atomicIndices, qualifyingResidues);
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iAtom of qualifyingAtoms) {
|
||||
AtomRanges.add(ranges, iAtom, iAtom + 1 as ElementIndex);
|
||||
ElementRanges.add(ranges, iAtom, iAtom + 1 as ElementIndex);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by any of `rows` (atoms that satisfy more rows are still included only once) */
|
||||
export function getAtomRangesForRows(model: Model, rows: MVSAnnotationRow | MVSAnnotationRow[], indices: IndicesAndSortings): AtomRanges {
|
||||
if (Array.isArray(rows)) {
|
||||
return AtomRanges.union(rows.map(row => getAtomRangesForRow(model, row, indices)));
|
||||
} else {
|
||||
return getAtomRangesForRow(model, rows, indices);
|
||||
}
|
||||
export function getAtomRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getAtomRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
|
||||
/** Return an array of chain indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): readonly ChainIndex[] {
|
||||
function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings): readonly ChainIndex[] {
|
||||
const { auth_asym_id, label_entity_id, _rowCount: nChains } = model.atomicHierarchy.chains;
|
||||
let result: readonly ChainIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_asym_id)) {
|
||||
@@ -105,10 +110,10 @@ function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: Indic
|
||||
}
|
||||
|
||||
/** Return an array of residue indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromChains: readonly ChainIndex[]): ResidueIndex[] {
|
||||
function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings, fromChains: readonly ChainIndex[]): ResidueIndex[] {
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = model.atomicHierarchy.residues;
|
||||
const { label_comp_id, auth_comp_id } = model.atomicHierarchy.atoms;
|
||||
const { residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const { residueAtomSegments, chainAtomSegments, residueSourceIndex } = model.atomicHierarchy;
|
||||
const { Present } = Column.ValueKind;
|
||||
const result: ResidueIndex[] = [];
|
||||
for (const iChain of fromChains) {
|
||||
@@ -125,6 +130,14 @@ function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: Ind
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.auth_seq_id);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.residue_index)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => residueSourceIndex.value(i) === row.residue_index);
|
||||
} else {
|
||||
const sorting = indices.residuesSortedBySourceIndex.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.residue_index);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.pdbx_PDB_ins_code)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => pdbx_PDB_ins_code.value(i) === row.pdbx_PDB_ins_code);
|
||||
@@ -195,11 +208,12 @@ function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: Ind
|
||||
}
|
||||
arrayExtend(result, residuesHere);
|
||||
}
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of atom indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromResidues: readonly ResidueIndex[]): ElementIndex[] {
|
||||
function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings, fromResidues: readonly ResidueIndex[]): ElementIndex[] {
|
||||
const { label_atom_id, auth_atom_id, type_symbol, label_comp_id, auth_comp_id } = model.atomicHierarchy.atoms;
|
||||
const residueAtomSegments_offsets = model.atomicHierarchy.residueAtomSegments.offsets;
|
||||
const result: ElementIndex[] = [];
|
||||
@@ -227,12 +241,12 @@ function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: Indice
|
||||
|
||||
/** Return index of atom in `model` which satistfies criteria given by `row`, if any.
|
||||
* Only works when `row.atom_id` and/or `row.atom_index` is defined (otherwise use `getAtomRangesForRow`). */
|
||||
function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): ElementIndex | undefined {
|
||||
function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: AtomicIndicesAndSortings): ElementIndex | undefined {
|
||||
let iAtom: ElementIndex | undefined = undefined;
|
||||
if (!isDefined(row.atom_id) && !isDefined(row.atom_index)) throw new Error('ArgumentError: at least one of row.atom_id, row.atom_index must be defined.');
|
||||
if (isDefined(row.atom_id) && isDefined(row.atom_index)) {
|
||||
const a1 = indices.atomsById.get(row.atom_id);
|
||||
const a2 = indices.atomsByIndex.get(row.atom_index);
|
||||
const a2 = indices.atomsBySourceIndex.get(row.atom_index);
|
||||
if (a1 !== a2) return undefined;
|
||||
iAtom = a1;
|
||||
}
|
||||
@@ -240,7 +254,7 @@ function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: IndicesA
|
||||
iAtom = indices.atomsById.get(row.atom_id);
|
||||
}
|
||||
if (isDefined(row.atom_index)) {
|
||||
iAtom = indices.atomsByIndex.get(row.atom_index);
|
||||
iAtom = indices.atomsBySourceIndex.get(row.atom_index);
|
||||
}
|
||||
if (iAtom === undefined) return undefined;
|
||||
if (!atomQualifies(model, iAtom, row)) return undefined;
|
||||
@@ -263,11 +277,13 @@ export function atomQualifies(model: Model, iAtom: ElementIndex, row: MVSAnnotat
|
||||
const label_seq_id = (h.residues.label_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.label_seq_id.value(iRes) : undefined;
|
||||
const auth_seq_id = (h.residues.auth_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.auth_seq_id.value(iRes) : undefined;
|
||||
const pdbx_PDB_ins_code = h.residues.pdbx_PDB_ins_code.value(iRes);
|
||||
const residue_index = h.residueSourceIndex.value(iRes);
|
||||
if (!matches(row.label_seq_id, label_seq_id)) return false;
|
||||
if (!matches(row.auth_seq_id, auth_seq_id)) return false;
|
||||
if (!matches(row.pdbx_PDB_ins_code, pdbx_PDB_ins_code)) return false;
|
||||
if (!matchesRange(row.beg_label_seq_id, row.end_label_seq_id, label_seq_id)) return false;
|
||||
if (!matchesRange(row.beg_auth_seq_id, row.end_auth_seq_id, auth_seq_id)) return false;
|
||||
if (!matches(row.residue_index, residue_index)) return false;
|
||||
|
||||
const label_comp_id = h.atoms.label_comp_id.value(iAtom);
|
||||
const auth_comp_id = h.atoms.auth_comp_id.value(iAtom);
|
||||
@@ -301,6 +317,124 @@ function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | und
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// COARSE SELECTIONS
|
||||
|
||||
/** Return sphere ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getSphereRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.spheres) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
return getCoarseElementRangesForRow(row, model.coarseHierarchy.spheres, indices.spheres);
|
||||
}
|
||||
|
||||
/** Return sphere ranges in `model` which satisfy criteria given by any of `rows` (spheres that satisfy more rows are still included only once) */
|
||||
export function getSphereRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getSphereRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
/** Return gaussian ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getGaussianRangesForRow(row: MVSAnnotationRow, model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges | undefined {
|
||||
if (!indices.gaussians) return undefined;
|
||||
if (isDefined(row.instance_id) && row.instance_id !== instanceId) return undefined;
|
||||
return getCoarseElementRangesForRow(row, model.coarseHierarchy.gaussians, indices.gaussians);
|
||||
}
|
||||
|
||||
/** Return gaussian ranges in `model` which satisfy criteria given by any of `rows` (gaussians that satisfy more rows are still included only once) */
|
||||
export function getGaussianRangesForRows(rows: MVSAnnotationRow[], model: Model, instanceId: string, indices: IndicesAndSortings): ElementRanges {
|
||||
return ElementRanges.union(rows.map(row => getGaussianRangesForRow(row, model, instanceId, indices)));
|
||||
}
|
||||
|
||||
/** Return ranges of coarse elements (spheres or gaussians) which satisfy criteria given by `row` */
|
||||
export function getCoarseElementRangesForRow(row: MVSAnnotationRow, coarseElements: CoarseElements, indices: CoarseIndicesAndSortings): ElementRanges | undefined {
|
||||
const nElements = coarseElements.count;
|
||||
if (nElements === 0) return undefined;
|
||||
|
||||
const hasResidueFilter = isAnyDefined(row.label_seq_id, row.beg_label_seq_id, row.end_label_seq_id);
|
||||
const hasChainFilter = isAnyDefined(row.label_asym_id, row.label_entity_id);
|
||||
const hasInvalidFilter = isAnyDefined(
|
||||
row.auth_asym_id,
|
||||
row.auth_seq_id, row.pdbx_PDB_ins_code, row.beg_auth_seq_id, row.end_auth_seq_id, row.label_comp_id, row.auth_comp_id, row.residue_index,
|
||||
row.label_atom_id, row.auth_atom_id, row.type_symbol, row.atom_id, row.atom_index);
|
||||
|
||||
if (hasInvalidFilter) {
|
||||
printCoarseSelectorWarning();
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!hasChainFilter && !hasResidueFilter) {
|
||||
return ElementRanges.single(0 as ElementIndex, nElements as ElementIndex);
|
||||
}
|
||||
|
||||
const qualifyingChains = getQualifyingCoarseChains(coarseElements, row, indices);
|
||||
if (!hasResidueFilter) {
|
||||
const chainOffsets = coarseElements.chainElementSegments.offsets;
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iChain of qualifyingChains) {
|
||||
ElementRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingElements = getQualifyingCoarseElements(coarseElements, row, indices, qualifyingChains);
|
||||
const ranges = ElementRanges.empty();
|
||||
for (const iElem of qualifyingElements) {
|
||||
ElementRanges.add(ranges, iElem, iElem + 1 as ElementIndex);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
|
||||
/** Return an array of chain indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingCoarseChains(coarseElements: CoarseElements, row: MVSAnnotationRow, indices: CoarseIndicesAndSortings): readonly ChainIndex[] {
|
||||
let result: readonly ChainIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_asym_id)) {
|
||||
result = indices.chainsByAsymId.get(row.label_asym_id) ?? EmptyArray;
|
||||
}
|
||||
if (isDefined(row.label_entity_id)) {
|
||||
if (result) {
|
||||
result = result.filter(iChain => coarseElements.entity_id.value(coarseElements.chainElementSegments.offsets[iChain]) === row.label_entity_id);
|
||||
} else {
|
||||
result = indices.chainsByEntityId.get(row.label_entity_id) ?? EmptyArray;
|
||||
}
|
||||
}
|
||||
result ??= range(coarseElements.chainElementSegments.count) as ChainIndex[];
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of residue indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingCoarseElements(coarseElements: CoarseElements, row: MVSAnnotationRow, indices: CoarseIndicesAndSortings, fromChains: readonly ChainIndex[]): ElementIndex[] {
|
||||
const result: ElementIndex[] = [];
|
||||
for (const iChain of fromChains) {
|
||||
const sorting = indices.elementsSortedBySeqIdBegin.get(iChain)!;
|
||||
const queryStart = Math.max(row.label_seq_id ?? -Infinity, row.beg_label_seq_id ?? -Infinity);
|
||||
const queryEnd = Math.min(row.label_seq_id ?? Infinity, row.end_label_seq_id ?? Infinity); // inclusive
|
||||
const iStart = SortedArray.findPredecessorIndex(sorting.endUpperBounds, queryStart); // select elements potentially ending >=queryStart (necessary condition)
|
||||
const iStop = SortedArray.findPredecessorIndex(sorting.values, queryEnd + 1); // select elements starting <=queryEnd (necessary and suffient condition) // exclusive
|
||||
|
||||
for (let i = iStart; i < iStop; i++) {
|
||||
const iElem = sorting.keys[i];
|
||||
if (coarseElements.seq_id_end.value(iElem) >= queryStart) { // rechecking seq_id_end, as the condition was not sufficient
|
||||
result.push(iElem);
|
||||
}
|
||||
}
|
||||
// This implementation can yield some elements even when queryStart>queryEnd (e.g. { beg_label_seq_id: 70, end_label_seq_id: 58, label_seq_id: 60 } -> sphere 51-100 qualifies ).
|
||||
// This is on purpose, to have the same behavior as MolScript.
|
||||
}
|
||||
sortIfNeeded(result, (a, b) => a - b);
|
||||
return result;
|
||||
}
|
||||
|
||||
let coarseSelectorWarningPrinted = false;
|
||||
function printCoarseSelectorWarning() {
|
||||
if (!coarseSelectorWarningPrinted) {
|
||||
console.warn('Using unsupported selector fields (auth_asym_id, auth_seq_id, pdbx_PDB_ins_code, beg_auth_seq_id, end_auth_seq_id, label_comp_id, auth_comp_id, residue_index, label_atom_id, auth_atom_id, type_symbol, atom_id, atom_index) on a coarse structure. The resulting selection will be empty.');
|
||||
coarseSelectorWarningPrinted = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// GENERAL
|
||||
|
||||
/** Convert an annotation row into a MolScript expression */
|
||||
export function rowToExpression(row: MVSAnnotationRow): Expression {
|
||||
return StructureElement.Schema.toExpression(row);
|
||||
@@ -332,7 +466,7 @@ export function groupRows(rows: readonly MVSAnnotationRow[]): GroupedArray<numbe
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const group_id = rows[i].group_id;
|
||||
if (group_id === undefined) {
|
||||
if (!isDefined(group_id)) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(group_id);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { hashString } from '../../../mol-data/util';
|
||||
import { StateObject } from '../../../mol-state';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { decodeColor as _decodeColor } from '../../../mol-util/color/utils';
|
||||
|
||||
|
||||
/** Represents either the result or the reason of failure of an operation that might have failed */
|
||||
@@ -99,45 +99,13 @@ export type ElementOfSet<S> = S extends Set<infer T> ? T : never
|
||||
|
||||
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
|
||||
* Return `undefined` if `colorString` cannot be converted. */
|
||||
export function decodeColor(colorString: string | undefined | null): Color | undefined {
|
||||
if (colorString === undefined || colorString === null) return undefined;
|
||||
let result: Color | undefined;
|
||||
if (HexColor.is(colorString)) {
|
||||
if (colorString.length === 4) {
|
||||
// convert short form to full form (#f0f -> #ff00ff)
|
||||
colorString = `#${colorString[1]}${colorString[1]}${colorString[2]}${colorString[2]}${colorString[3]}${colorString[3]}`;
|
||||
}
|
||||
result = Color.fromHexStyle(colorString);
|
||||
if (result !== undefined && !isNaN(result)) return result;
|
||||
export function decodeColor(colorString: string | number | undefined | null): Color | undefined {
|
||||
if (typeof colorString === 'number') {
|
||||
return Color(colorString);
|
||||
}
|
||||
result = ColorNames[colorString.toLowerCase() as keyof typeof ColorNames];
|
||||
if (result !== undefined) return result;
|
||||
return undefined;
|
||||
return _decodeColor(colorString);
|
||||
}
|
||||
|
||||
/** Regular expression matching a hexadecimal color string, e.g. '#FF1100' or '#f10' */
|
||||
const hexColorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
|
||||
/** Hexadecimal color string, e.g. '#FF1100' (the type matches more than just valid HexColor strings) */
|
||||
export type HexColor = `#${string}`
|
||||
|
||||
export const HexColor = {
|
||||
/** Decide if a string is a valid hexadecimal color string (6-digit or 3-digit, e.g. '#FF1100' or '#f10') */
|
||||
is(str: any): str is HexColor {
|
||||
return typeof str === 'string' && hexColorRegex.test(str);
|
||||
},
|
||||
};
|
||||
|
||||
/** Named color string, e.g. 'red' */
|
||||
export type ColorName = keyof ColorNames
|
||||
|
||||
export const ColorName = {
|
||||
/** Decide if a string is a valid named color string */
|
||||
is(str: any): str is ColorName {
|
||||
return str in ColorNames;
|
||||
},
|
||||
};
|
||||
|
||||
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
|
||||
const ret: any = {};
|
||||
|
||||
@@ -160,4 +128,25 @@ export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], depe
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function getMVSReferenceObject<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject> | undefined, ref: string): StateObject | undefined {
|
||||
if (!dependencies) return undefined;
|
||||
|
||||
for (const key of Object.keys(dependencies)) {
|
||||
const o = dependencies[key];
|
||||
let okType = false;
|
||||
for (const t of type) {
|
||||
if (t.is(o)) {
|
||||
okType = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!okType || !o.tags) continue;
|
||||
for (const tag of o.tags) {
|
||||
if (tag.startsWith('mvs-ref:')) {
|
||||
if (tag.substring(8) === ref) return o;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/extensions/mvs/index.ts
Normal file
8
src/extensions/mvs/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export * from './mvs-data';
|
||||
export * from './load';
|
||||
@@ -46,7 +46,7 @@ export function loadTreeVirtual<TTree extends Tree, TContext>(
|
||||
) {
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
|
||||
const stateTree: StateTree = updateRoot.update.getTree();
|
||||
const stateTree: StateTree = updateRoot.update.getTree({ useHashVersion: true });
|
||||
const stateSnapshot: State.Snapshot = { tree: StateTree.toJSON(stateTree) };
|
||||
const pluginStateSnapshot: PluginState.Snapshot = { id: UUID.create22(), data: stateSnapshot };
|
||||
return pluginStateSnapshot;
|
||||
|
||||
@@ -5,16 +5,20 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Mat3, Mat4, Quat, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { StructureComponentParams } from '../../mol-plugin-state/helpers/structure-component';
|
||||
import { StructureFromModel, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureFromModel, StructureInstances, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeInstances, VolumeTransform } from '../../mol-plugin-state/transforms/volume';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { arrayDistinct } from '../../mol-util/array';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorListEntry } from '../../mol-util/color/color';
|
||||
import { canonicalJsonString } from '../../mol-util/json';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { MVSAnnotationColorThemeProps, MVSAnnotationColorThemeProvider } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationColorThemeProps, MVSAnnotationColorThemeProvider, MVSCategoricalPaletteProps, MVSContinuousPaletteProps, MVSDiscretePaletteProps } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationSpec } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponentProps } from './components/annotation-structure-component';
|
||||
@@ -23,13 +27,16 @@ import { CustomLabelTextProps } from './components/custom-label/visual';
|
||||
import { CustomTooltipsProps } from './components/custom-tooltips-prop';
|
||||
import { MultilayerColorThemeName, MultilayerColorThemeProps, NoColor } from './components/multilayer-color-theme';
|
||||
import { SelectorAll } from './components/selector';
|
||||
import { MvsNamedColorDicts, MvsNamedColorLists } from './helpers/colors';
|
||||
import { rowToExpression, rowsToExpression } from './helpers/selections';
|
||||
import { ElementOfSet, decodeColor, isDefined, stringHash } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { mvsRefTags, UpdateTarget } from './load-generic';
|
||||
import { Subtree, getChildren } from './tree/generic/tree-schema';
|
||||
import { dfs, formatObject } from './tree/generic/tree-utils';
|
||||
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
|
||||
import { DefaultColor } from './tree/mvs/mvs-tree';
|
||||
import { CategoricalPalette, CategoricalPaletteDefaults, ColorDictNameT, ColorListNameT, ContinuousPalette, ContinuousPaletteDefaults, DiscretePalette, DiscretePaletteDefaults } from './tree/mvs/param-types';
|
||||
|
||||
|
||||
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
|
||||
@@ -55,6 +62,19 @@ export function transformFromRotationTranslation(rotation: number[] | null | und
|
||||
return T;
|
||||
}
|
||||
|
||||
export function decomposeRotationMatrix(rotation: number[] | null | undefined) {
|
||||
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
|
||||
if (rotation) {
|
||||
const rotMatrix = Mat3.fromArray(Mat3(), rotation, 0);
|
||||
ensureRotationMatrix(rotMatrix, rotMatrix);
|
||||
const quat = Quat.fromMat3(Quat(), rotMatrix);
|
||||
const axis = Vec3();
|
||||
const angle = Quat.getAxisAngle(axis, quat) * 180 / Math.PI;
|
||||
return { axis, angle };
|
||||
}
|
||||
return { axis: Vec3.create(1, 0, 0), angle: 0 };
|
||||
}
|
||||
|
||||
/** Adjust values in a close-to-rotation matrix `a` to ensure it is a proper rotation matrix
|
||||
* (i.e. its columns and rows are orthonormal and determinant equal to 1, within available precission). */
|
||||
function ensureRotationMatrix(out: Mat3, a: Mat3) {
|
||||
@@ -71,15 +91,65 @@ const _tmpVecX = Vec3();
|
||||
const _tmpVecY = Vec3();
|
||||
const _tmpVecZ = Vec3();
|
||||
|
||||
/** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */
|
||||
export function transformProps(node: MolstarSubtree<'structure'>): StateTransformer.Params<TransformStructureConformation>[] {
|
||||
const result = [] as StateTransformer.Params<TransformStructureConformation>[];
|
||||
const transforms = getChildren(node).filter(c => c.kind === 'transform') as MolstarNode<'transform'>[];
|
||||
for (const transform of transforms) {
|
||||
const { rotation, translation } = transform.params;
|
||||
const matrix = transformFromRotationTranslation(rotation, translation);
|
||||
result.push({ transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
|
||||
export function transformAndInstantiateStructure(
|
||||
target: UpdateTarget,
|
||||
node: MolstarSubtree<'structure' | 'component' | 'component_from_source' | 'component_from_uri'>,
|
||||
) {
|
||||
return applyTransformAndInstances(target, node, TransformStructureConformation, StructureInstances);
|
||||
}
|
||||
|
||||
export function transformAndInstantiateVolume(target: UpdateTarget, node: MolstarSubtree<'volume'>) {
|
||||
return applyTransformAndInstances(target, node, VolumeTransform, VolumeInstances);
|
||||
}
|
||||
|
||||
function applyTransformAndInstances(target: UpdateTarget, node: MolstarSubtree, transform: StateTransformer, instantiate: StateTransformer) {
|
||||
let modified = target;
|
||||
for (const { params, ref } of transformProps(node, 'transform')) {
|
||||
modified = UpdateTarget.apply(modified, transform, params);
|
||||
UpdateTarget.tag(modified, mvsRefTags(ref));
|
||||
}
|
||||
|
||||
const instances = transformProps(node, 'instance');
|
||||
if (instances.length > 0) {
|
||||
modified = UpdateTarget.apply(modified, instantiate, { transforms: instances.map(i => i.params) });
|
||||
}
|
||||
|
||||
return modified;
|
||||
}
|
||||
|
||||
/** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */
|
||||
function transformProps(node: MolstarSubtree, kind: 'transform' | 'instance') {
|
||||
const result = [] as { params: StateTransformer.Params<TransformStructureConformation>, ref?: string }[];
|
||||
const transforms = getChildren(node).filter(c => c.kind === kind) as MolstarNode<'transform'>[];
|
||||
for (const transform of transforms) {
|
||||
let matrix: Mat4 | undefined = transform.params.matrix as Mat4 | undefined;
|
||||
if (!matrix) {
|
||||
const { rotation, translation, rotation_center } = transform.params;
|
||||
if (rotation_center) {
|
||||
const axisAngle = decomposeRotationMatrix(rotation);
|
||||
result.push({
|
||||
params: {
|
||||
transform: {
|
||||
name: 'components',
|
||||
params: {
|
||||
translation: translation ? Vec3.fromArray(Vec3(), translation, 0) : Vec3.create(0, 0, 0),
|
||||
angle: axisAngle.angle,
|
||||
axis: axisAngle.axis,
|
||||
rotationCenter: rotation_center === 'centroid'
|
||||
? { name: 'centroid', params: {} }
|
||||
: { name: 'point', params: { point: Vec3.fromArray(Vec3(), rotation_center, 0) } }
|
||||
}
|
||||
}
|
||||
},
|
||||
ref: transform.ref
|
||||
});
|
||||
continue;
|
||||
}
|
||||
matrix = transformFromRotationTranslation(rotation, translation);
|
||||
}
|
||||
result.push({ params: { transform: { name: 'matrix', params: { data: matrix, transpose: false } } }, ref: transform.ref });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -90,10 +160,18 @@ export function collectAnnotationReferences(tree: Subtree<MolstarTree>, context:
|
||||
let spec: Omit<MVSAnnotationSpec, 'id'> | undefined = undefined;
|
||||
if (AnnotationFromUriKinds.has(node.kind as any)) {
|
||||
const p = (node as MolstarNode<AnnotationFromUriKind>).params;
|
||||
spec = { source: { name: 'url', params: { url: p.uri, format: p.format } }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name ?? undefined };
|
||||
spec = {
|
||||
source: { name: 'url', params: { url: p.uri, format: p.format } }, schema: p.schema,
|
||||
cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name,
|
||||
fieldRemapping: Object.entries(p.field_remapping).map(([key, value]) => ({ standardName: key, actualName: value })),
|
||||
};
|
||||
} else if (AnnotationFromSourceKinds.has(node.kind as any)) {
|
||||
const p = (node as MolstarNode<AnnotationFromSourceKind>).params;
|
||||
spec = { source: { name: 'source-cif', params: {} }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name ?? undefined };
|
||||
spec = {
|
||||
source: { name: 'source-cif', params: {} }, schema: p.schema,
|
||||
cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name,
|
||||
fieldRemapping: Object.entries(p.field_remapping).map(([key, value]) => ({ standardName: key, actualName: value })),
|
||||
};
|
||||
}
|
||||
if (spec) {
|
||||
const key = canonicalJsonString(spec as any);
|
||||
@@ -141,7 +219,7 @@ export function collectInlineTooltips(tree: MolstarSubtree<'structure'>, context
|
||||
text: node.params.text,
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues, label: p.label || 'Annotation' },
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -175,7 +253,7 @@ export function collectInlineLabels(tree: MolstarSubtree<'structure'>, context:
|
||||
params: {
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues, label: p.label || 'Annotation' },
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -282,7 +360,7 @@ export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'co
|
||||
}
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a representation node. */
|
||||
export function representationProps(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
function representationPropsBase(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const alpha = alphaForNode(node);
|
||||
const params = node.params;
|
||||
switch (params.type) {
|
||||
@@ -291,10 +369,20 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
type: { name: 'cartoon', params: { alpha, tubularHelices: params.tubular_helices } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'backbone':
|
||||
return {
|
||||
type: { name: 'backbone', params: { alpha } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: (params.size_factor ?? 1) * 0.5, sizeAspectRatio: 0.5, alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
};
|
||||
case 'line':
|
||||
return {
|
||||
type: { name: 'line', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
sizeTheme: { name: 'uniform', params: { value: params.size_factor } },
|
||||
};
|
||||
case 'spacefill':
|
||||
return {
|
||||
type: { name: 'spacefill', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
@@ -304,16 +392,32 @@ export function representationProps(node: MolstarSubtree<'representation'>): Par
|
||||
return {
|
||||
type: { name: 'carbohydrate', params: { alpha, sizeFactor: params.size_factor ?? 1 } },
|
||||
};
|
||||
case 'surface':
|
||||
case 'surface': {
|
||||
return {
|
||||
type: { name: 'molecular-surface', params: { alpha, ignoreHydrogens: params.ignore_hydrogens } },
|
||||
type: {
|
||||
name: params.surface_type === 'gaussian' ? 'gaussian-surface' : 'molecular-surface',
|
||||
params: { alpha, ignoreHydrogens: params.ignore_hydrogens }
|
||||
},
|
||||
sizeTheme: { name: 'physical', params: { scale: params.size_factor } },
|
||||
};
|
||||
}
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
}
|
||||
|
||||
export function representationProps(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const base = representationPropsBase(node);
|
||||
const clip = clippingForNode(node);
|
||||
if (clip) {
|
||||
base.type!.params = { ...base.type?.params, clip };
|
||||
}
|
||||
if (node.custom?.molstar_representation_params) {
|
||||
base.type!.params = { ...base.type!.params, ...node.custom.molstar_representation_params };
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */
|
||||
export function alphaForNode(node: MolstarSubtree<'representation' | 'volume_representation'>): number {
|
||||
const children = getChildren(node).filter(c => c.kind === 'opacity');
|
||||
@@ -324,13 +428,82 @@ export function alphaForNode(node: MolstarSubtree<'representation' | 'volume_rep
|
||||
}
|
||||
}
|
||||
|
||||
function getCommonClipParams(node: MolstarNode<'clip'>): Pick<Clip.Props['objects'][number], 'invert' | 'transform'> {
|
||||
return {
|
||||
invert: !!node.params.invert,
|
||||
transform: node.params.check_transform ? Mat4.fromArray(Mat4(), node.params.check_transform, 0) : Mat4.identity(),
|
||||
};
|
||||
}
|
||||
|
||||
function getClipObject(node: MolstarNode<'clip'>): Clip.Props['objects'][number] | undefined {
|
||||
switch (node.params.type) {
|
||||
case 'sphere':
|
||||
return {
|
||||
type: 'sphere',
|
||||
position: Vec3.ofArray(node.params.center),
|
||||
scale: typeof node.params.radius === 'number'
|
||||
? Vec3.create(2 * node.params.radius, 2 * node.params.radius, 2 * node.params.radius)
|
||||
: Vec3.create(2, 2, 2),
|
||||
rotation: { axis: Vec3.create(1, 0, 0), angle: 0 },
|
||||
...getCommonClipParams(node),
|
||||
};
|
||||
case 'plane': {
|
||||
const up = Vec3.create(0, 1, 0);
|
||||
const n = Vec3.normalize(Vec3(), Vec3.ofArray(node.params.normal));
|
||||
const axis = Vec3.cross(Vec3(), up, n);
|
||||
const isSingular = Vec3.magnitude(axis) < 1e-6;
|
||||
return {
|
||||
type: 'plane',
|
||||
position: Vec3.ofArray(node.params.point),
|
||||
scale: Vec3.create(1, 1, 1),
|
||||
rotation: {
|
||||
axis: isSingular ? Vec3.unitX : axis,
|
||||
angle: isSingular ? 0 : Vec3.angle(up, n) * 180 / Math.PI,
|
||||
},
|
||||
...getCommonClipParams(node),
|
||||
};
|
||||
}
|
||||
case 'box':
|
||||
const q = Quat.fromMat3(Quat(), Mat3.fromArray(Mat3(), node.params.rotation, 0));
|
||||
const axis = Vec3();
|
||||
const angle = Quat.getAxisAngle(axis, q) * 180 / Math.PI;
|
||||
return {
|
||||
type: 'cube',
|
||||
position: Vec3.ofArray(node.params.center),
|
||||
scale: Vec3.ofArray(node.params.size),
|
||||
rotation: { axis, angle },
|
||||
...getCommonClipParams(node),
|
||||
};
|
||||
default:
|
||||
console.warn(`Mol* MVS: Unsupported clip type "${(node as MolstarNode<'clip'>).params.type}" in node ${node.ref}.`);
|
||||
}
|
||||
}
|
||||
|
||||
export function clippingForNode(node: MolstarSubtree<'representation' | 'volume_representation' | 'primitives' | 'primitives_from_uri'>): Clip.Props | undefined {
|
||||
const children = getChildren(node).filter(c => c.kind === 'clip');
|
||||
if (!children.length) return;
|
||||
|
||||
const variant = children[0].params.variant === 'object' ? 'instance' : 'pixel';
|
||||
const objects: Clip.Props['objects'] = children.map(getClipObject).filter(o => !!o);
|
||||
|
||||
return { variant, objects } satisfies Clip.Props;
|
||||
}
|
||||
|
||||
function hasMolStarUseDefaultColoring(node: MolstarNode): boolean {
|
||||
if (!node.custom) return false;
|
||||
return 'molstar_use_default_coloring' in node.custom || 'molstar_color_theme_name' in node.custom;
|
||||
}
|
||||
|
||||
function customColoring(custom: any) {
|
||||
if (custom?.molstar_use_default_coloring) return undefined;
|
||||
return {
|
||||
name: custom?.molstar_color_theme_name ?? undefined,
|
||||
params: custom?.molstar_color_theme_params ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] | undefined {
|
||||
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation' | 'volume'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] | undefined {
|
||||
if (node?.kind === 'representation') {
|
||||
const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source') as MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>[];
|
||||
if (children.length === 0) {
|
||||
@@ -339,12 +512,7 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
params: { value: decodeColor(DefaultColor) },
|
||||
};
|
||||
} else if (children.length === 1 && hasMolStarUseDefaultColoring(children[0])) {
|
||||
if (children[0].custom?.molstar_use_default_coloring) return undefined;
|
||||
const custom = children[0].custom;
|
||||
return {
|
||||
name: custom?.molstar_color_theme_name ?? undefined,
|
||||
params: custom?.molstar_color_theme_params ?? {},
|
||||
};
|
||||
return customColoring(children[0].custom);
|
||||
} else if (children.length === 1 && appliesColorToWholeRepr(children[0])) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
} else {
|
||||
@@ -361,31 +529,31 @@ export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri
|
||||
};
|
||||
}
|
||||
}
|
||||
let annotationId: string | undefined = undefined;
|
||||
let fieldName: string | undefined = undefined;
|
||||
let color: string | undefined = undefined;
|
||||
switch (node?.kind) {
|
||||
case 'color_from_uri':
|
||||
case 'color_from_source':
|
||||
annotationId = context.annotationMap.get(node);
|
||||
fieldName = node.params.field_name;
|
||||
break;
|
||||
case 'color':
|
||||
color = node.params.color;
|
||||
break;
|
||||
}
|
||||
if (annotationId) {
|
||||
return {
|
||||
name: MVSAnnotationColorThemeProvider.name,
|
||||
params: { annotationId, fieldName, background: NoColor } satisfies Partial<MVSAnnotationColorThemeProps>,
|
||||
};
|
||||
} else {
|
||||
if (node?.kind === 'color') {
|
||||
if (hasMolStarUseDefaultColoring(node)) {
|
||||
return customColoring(node.custom);
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(color) },
|
||||
params: { value: decodeColor(node.params.color) },
|
||||
};
|
||||
}
|
||||
if (node?.kind === 'color_from_uri' || node?.kind === 'color_from_source') {
|
||||
const annotationId = context.annotationMap.get(node);
|
||||
if (annotationId === undefined) return {
|
||||
name: 'uniform',
|
||||
params: {},
|
||||
};
|
||||
|
||||
const fieldName = node.params.field_name;
|
||||
return {
|
||||
name: MVSAnnotationColorThemeProvider.name,
|
||||
params: { annotationId, fieldName, background: NoColor, palette: palettePropsFromMVSPalette(node.params.palette) } satisfies Partial<MVSAnnotationColorThemeProps>,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>): boolean {
|
||||
if (node.kind === 'color') {
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
@@ -394,6 +562,153 @@ function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' |
|
||||
}
|
||||
}
|
||||
|
||||
const FALLBACK_COLOR = decodeColor(DefaultColor)!;
|
||||
|
||||
export function palettePropsFromMVSPalette(palette: MolstarNode<'color_from_uri' | 'color_from_source'>['params']['palette']): MVSAnnotationColorThemeProps['palette'] {
|
||||
if (!palette) {
|
||||
return { name: 'direct', params: {} };
|
||||
}
|
||||
if (palette.kind === 'categorical') {
|
||||
const fullParams: Required<CategoricalPalette> = objMerge(CategoricalPaletteDefaults, palette);
|
||||
return {
|
||||
name: 'categorical',
|
||||
params: {
|
||||
colors: categoricalPalettePropsFromMVSColors(fullParams.colors),
|
||||
repeatColorList: fullParams.repeat_color_list,
|
||||
sort: fullParams.sort,
|
||||
sortDirection: fullParams.sort_direction,
|
||||
caseInsensitive: fullParams.case_insensitive,
|
||||
setMissingColor: !!fullParams.missing_color,
|
||||
missingColor: decodeColor(fullParams.missing_color) ?? FALLBACK_COLOR,
|
||||
} satisfies MVSCategoricalPaletteProps,
|
||||
};
|
||||
}
|
||||
if (palette.kind === 'discrete') {
|
||||
const fullParams: Required<DiscretePalette> = objMerge(DiscretePaletteDefaults, palette);
|
||||
return {
|
||||
name: 'discrete',
|
||||
params: {
|
||||
colors: discretePalettePropsFromMVSColors(fullParams.colors, fullParams.reverse),
|
||||
mode: fullParams.mode,
|
||||
xMin: fullParams.value_domain[0],
|
||||
xMax: fullParams.value_domain[1],
|
||||
} satisfies MVSDiscretePaletteProps,
|
||||
};
|
||||
}
|
||||
if (palette.kind === 'continuous') {
|
||||
const fullParams: Required<ContinuousPalette> = objMerge(ContinuousPaletteDefaults, palette);
|
||||
const colors = continuousPalettePropsFromMVSColors(fullParams.colors, fullParams.reverse);
|
||||
return {
|
||||
name: 'continuous',
|
||||
params: {
|
||||
colors: colors,
|
||||
mode: fullParams.mode,
|
||||
xMin: fullParams.value_domain[0],
|
||||
xMax: fullParams.value_domain[1],
|
||||
setUnderflowColor: !!fullParams.underflow_color,
|
||||
underflowColor: (fullParams.underflow_color === 'auto' ? minColor(colors.colors) : decodeColor(fullParams.underflow_color)) ?? FALLBACK_COLOR,
|
||||
setOverflowColor: !!fullParams.overflow_color,
|
||||
overflowColor: (fullParams.overflow_color === 'auto' ? maxColor(colors.colors) : decodeColor(fullParams.overflow_color)) ?? FALLBACK_COLOR,
|
||||
} satisfies MVSContinuousPaletteProps,
|
||||
};
|
||||
}
|
||||
throw new Error(`NotImplementedError: palettePropsFromMVSPalette is not implemented for palette kind "${(palette as any).kind}"`);
|
||||
}
|
||||
|
||||
/** Merge properties of two object into a new object. Property values from `second` override those from `first`, but `undefined` is treated as if property missing while `null` as a regular value. */
|
||||
function objMerge<T extends object, U extends object>(first: T, second: U): T & U {
|
||||
const out: Partial<T & U> = { ...first };
|
||||
for (const key in second) {
|
||||
const value = second[key];
|
||||
if (value !== undefined) out[key] = value as any;
|
||||
}
|
||||
return out as T & U;
|
||||
}
|
||||
|
||||
function categoricalPalettePropsFromMVSColors(colors: Required<CategoricalPalette>['colors']): MVSCategoricalPaletteProps['colors'] {
|
||||
if (typeof colors === 'string') {
|
||||
if (colors in MvsNamedColorLists) {
|
||||
const colorList = MvsNamedColorLists[colors as ColorListNameT];
|
||||
return { name: 'list', params: { kind: 'set', colors: colorList.list } };
|
||||
}
|
||||
if (colors in MvsNamedColorDicts) {
|
||||
const colorDict = MvsNamedColorDicts[colors as ColorDictNameT];
|
||||
return { name: 'dictionary', params: Object.entries(colorDict).map(([value, color]) => ({ value, color })) };
|
||||
}
|
||||
console.warn(`Could not find named color palette "${colors}"`);
|
||||
}
|
||||
if (Array.isArray(colors)) {
|
||||
return { name: 'list', params: { kind: 'set', colors: colors.map(c => decodeColor(c) ?? FALLBACK_COLOR) } };
|
||||
}
|
||||
if (typeof colors === 'object') {
|
||||
return { name: 'dictionary', params: Object.entries(colors).map(([value, color]) => ({ value, color: decodeColor(color) ?? FALLBACK_COLOR })) };
|
||||
}
|
||||
return { name: 'list', params: { kind: 'set', colors: [] } };
|
||||
}
|
||||
|
||||
function discretePalettePropsFromMVSColors(colors: Required<DiscretePalette>['colors'], reverse: boolean): MVSDiscretePaletteProps['colors'] {
|
||||
if (typeof colors === 'string') {
|
||||
if (colors in MvsNamedColorLists) {
|
||||
const colorList = MvsNamedColorLists[colors];
|
||||
const list = reverse ? colorList.list.slice().reverse() : colorList.list;
|
||||
const sectionLength = 1 / list.length;
|
||||
return list.map((e, i) => ({ color: Color.fromColorListEntry(e), fromValue: i * sectionLength, toValue: (i + 1) * sectionLength }));
|
||||
}
|
||||
console.warn(`Could not find named color palette "${colors}"`);
|
||||
}
|
||||
if (Array.isArray(colors) && colors.every(t => typeof t === 'string')) {
|
||||
const list = reverse ? colors.slice().reverse() : colors;
|
||||
const sectionLength = 1 / colors.length;
|
||||
return list.map((c, i) => ({ color: decodeColor(c) ?? NoColor, fromValue: i * sectionLength, toValue: (i + 1) * sectionLength }));
|
||||
}
|
||||
if (Array.isArray(colors) && colors.every(t => Array.isArray(t) && t.length === 2)) {
|
||||
return colors.map((t, i) => ({ color: decodeColor(t[0]) ?? NoColor, fromValue: t[1], toValue: colors[i + 1]?.[1] ?? Infinity }));
|
||||
}
|
||||
if (Array.isArray(colors) && colors.every(t => Array.isArray(t) && t.length === 3)) {
|
||||
return colors.map(t => ({ color: decodeColor(t[0]) ?? NoColor, fromValue: t[1] ?? -Infinity, toValue: t[2] ?? Infinity }));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function continuousPalettePropsFromMVSColors(colors: Required<ContinuousPalette>['colors'], reverse: boolean): MVSContinuousPaletteProps['colors'] {
|
||||
if (typeof colors === 'string') {
|
||||
// Named color list
|
||||
if (colors in MvsNamedColorLists) {
|
||||
const colorList = MvsNamedColorLists[colors];
|
||||
const list = reverse ? colorList.list.slice().reverse() : colorList.list;
|
||||
const n = list.length - 1;
|
||||
return { kind: 'interpolate', colors: list.map((col, i) => [Color.fromColorListEntry(col), i / n]) };
|
||||
}
|
||||
console.warn(`Could not find named color palette "${colors}"`);
|
||||
}
|
||||
if (Array.isArray(colors)) {
|
||||
if (colors.every(t => Array.isArray(t))) {
|
||||
// Color list with checkpoints
|
||||
// Not applying `reverse` here, as it would have no effect
|
||||
return { kind: 'interpolate', colors: colors.map(t => [decodeColor(t[0]) ?? FALLBACK_COLOR, t[1]]) };
|
||||
} else {
|
||||
// Color list without checkpoints
|
||||
const list = reverse ? colors.slice().reverse() : colors;
|
||||
const n = list.length - 1;
|
||||
return { kind: 'interpolate', colors: list.map((col, i) => [decodeColor(col) ?? FALLBACK_COLOR, i / n]) };
|
||||
}
|
||||
}
|
||||
return { kind: 'interpolate', colors: [] };
|
||||
}
|
||||
|
||||
/** Return the color with the lowest checkpoint, or the first color if checkpoints not available. */
|
||||
function minColor(colors: ColorListEntry[]): Color | undefined {
|
||||
if (colors.length === 0) return undefined;
|
||||
if (colors.every(t => Array.isArray(t))) return Color.fromColorListEntry(colors.reduce((a, b) => a[1] < b[1] ? a : b));
|
||||
return Color.fromColorListEntry(colors[0]);
|
||||
}
|
||||
/** Return the color with the highest checkpoint, or the last color if checkpoints not available. */
|
||||
function maxColor(colors: ColorListEntry[]): Color | undefined {
|
||||
if (colors.length === 0) return undefined;
|
||||
if (colors.every(t => Array.isArray(t))) return Color.fromColorListEntry(colors.reduce((a, b) => a[1] > b[1] ? a : b));
|
||||
return Color.fromColorListEntry(colors[colors.length - 1]);
|
||||
}
|
||||
|
||||
/** Create a mapping of nearest representation nodes for each node in the tree
|
||||
* (to transfer coloring to label nodes smartly).
|
||||
* Only considers nodes within the same 'structure' subtree. */
|
||||
@@ -420,15 +735,26 @@ export function makeNearestReprMap(root: MolstarTree) {
|
||||
/** Create props for `VolumeRepresentation3D` transformer from a representation node. */
|
||||
export function volumeRepresentationProps(node: MolstarSubtree<'volume_representation'>): Partial<StateTransformer.Params<VolumeRepresentation3D>> {
|
||||
const alpha = alphaForNode(node);
|
||||
const clip = clippingForNode(node);
|
||||
const params = node.params;
|
||||
|
||||
const isoValue = typeof params.absolute_isovalue === 'number' ? Volume.IsoValue.absolute(params.absolute_isovalue) : Volume.IsoValue.relative(params.relative_isovalue ?? 0);
|
||||
switch (params.type) {
|
||||
case 'isosurface':
|
||||
const isoValue = typeof params.absolute_isovalue === 'number' ? Volume.IsoValue.absolute(params.absolute_isovalue) : Volume.IsoValue.relative(params.relative_isovalue ?? 0);
|
||||
const visuals: ('wireframe' | 'solid')[] = [];
|
||||
if (params.show_wireframe) visuals.push('wireframe');
|
||||
if (params.show_faces) visuals.push('solid');
|
||||
return {
|
||||
type: { name: 'isosurface', params: { alpha, isoValue, visuals } },
|
||||
type: { name: 'isosurface', params: { alpha, isoValue, visuals, clip } },
|
||||
};
|
||||
case 'grid_slice':
|
||||
const isRelative = params.relative_index !== undefined;
|
||||
const dimension = {
|
||||
name: isRelative ? `relative${params.dimension.toUpperCase()}` : params.dimension,
|
||||
params: params.relative_index ?? params.relative_index
|
||||
};
|
||||
return {
|
||||
type: { name: 'slice', params: { alpha, dimension, isoValue, clip } },
|
||||
};
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
@@ -448,4 +774,4 @@ export function volumeColorThemeForNode(node: MolstarSubtree<'volume_representat
|
||||
} if (children.length === 1) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,28 +8,34 @@
|
||||
|
||||
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Download, ParseCcp4, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif } from '../../mol-plugin-state/transforms/volume';
|
||||
import { Download, ParseCcp4, ParseCif, ParseDx, ParsePrmtop, ParsePsf, ParseTop } from '../../mol-plugin-state/transforms/data';
|
||||
import { CoordinatesFromDcd, CoordinatesFromLammpstraj, CoordinatesFromNctraj, CoordinatesFromTrr, CoordinatesFromXtc, CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TopologyFromPrmtop, TopologyFromPsf, TopologyFromTop, TrajectoryFromGRO, TrajectoryFromLammpsTrajData, TrajectoryFromMmCif, TrajectoryFromMOL, TrajectoryFromMOL2, TrajectoryFromPDB, TrajectoryFromSDF, TrajectoryFromXYZ } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D, VolumeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { VolumeFromCcp4, VolumeFromDensityServerCif, VolumeFromDx } from '../../mol-plugin-state/transforms/volume';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { PluginState } from '../../mol-plugin/state';
|
||||
import { StateObjectSelector, StateTree } from '../../mol-state';
|
||||
import { RuntimeContext, Task } from '../../mol-task';
|
||||
import { Clip } from '../../mol-util/clip';
|
||||
import { MolViewSpec } from './behavior';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps } from './camera';
|
||||
import { createPluginStateSnapshotCamera, modifyCanvasProps, resetCanvasProps } from './camera';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
import { CustomLabelProps, CustomLabelRepresentationProvider } from './components/custom-label/representation';
|
||||
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
|
||||
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData, MVSShapeRepresentation3D } from './components/primitives';
|
||||
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 { LoadingActions, LoadingExtension, loadTreeVirtual, UpdateTarget } from './load-generic';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps, volumeColorThemeForNode, volumeRepresentationProps } from './load-helpers';
|
||||
import { MVSData, MVSData_States, SnapshotMetadata } from './mvs-data';
|
||||
import { validateTree } from './tree/generic/tree-schema';
|
||||
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';
|
||||
import { MVSAnimationNode, MVSAnimationSchema } from './tree/animation/animation-tree';
|
||||
import { validateTree } from './tree/generic/tree-validation';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
@@ -46,27 +52,54 @@ export interface MVSLoadOptions {
|
||||
sanityChecks?: boolean,
|
||||
/** Base for resolving relative URLs/URIs. May itself be a relative URL (relative to the window URL). */
|
||||
sourceUrl?: string,
|
||||
|
||||
doNotReportErrors?: boolean
|
||||
}
|
||||
|
||||
export function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
const task = Task.create('Load MVS', ctx => _loadMVS(ctx, plugin, data, options));
|
||||
return plugin.runTask(task);
|
||||
}
|
||||
|
||||
/** Load a MolViewSpec (MVS) state(s) into the Mol* plugin as plugin state snapshots. */
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
async function _loadMVS(ctx: RuntimeContext, plugin: PluginContext, data: MVSData, options: MVSLoadOptions = {}) {
|
||||
plugin.errorContext.clear('mvs');
|
||||
try {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
|
||||
// Stop any currently running audio
|
||||
plugin.managers.markdownExtensions.audio.dispose();
|
||||
|
||||
// Reset canvas props to default so that modifyCanvasProps works as expected
|
||||
resetCanvasProps(plugin);
|
||||
|
||||
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
|
||||
const multiData: MVSData_States = data.kind === 'multiple' ? data : MVSData.stateToStates(data);
|
||||
const entries: PluginStateSnapshotManager.Entry[] = [];
|
||||
for (let i = 0; i < multiData.snapshots.length; i++) {
|
||||
const snapshot = multiData.snapshots[i];
|
||||
const previousSnapshot = i > 0 ? multiData.snapshots[i - 1] : multiData.snapshots[multiData.snapshots.length - 1];
|
||||
validateTree(MVSTreeSchema, snapshot.root, 'MVS');
|
||||
validateTree(MVSTreeSchema, snapshot.root, 'MVS', plugin);
|
||||
if (snapshot.animation) {
|
||||
validateTree(MVSAnimationSchema, snapshot.animation, 'Animation', plugin);
|
||||
}
|
||||
if (options.sanityChecks) mvsSanityCheck(snapshot.root);
|
||||
const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl);
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
const entry = molstarTreeToEntry(plugin, molstarTree, { ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms }, options);
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar', plugin);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
snapshot.animation,
|
||||
{ ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms },
|
||||
options
|
||||
);
|
||||
await assignStateTransition(ctx, plugin, entry, snapshot, options, i, multiData.snapshots.length);
|
||||
entries.push(entry);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'Loading MVS...', current: i, max: multiData.snapshots.length });
|
||||
}
|
||||
}
|
||||
if (!options.appendSnapshots) {
|
||||
plugin.managers.snapshot.clear();
|
||||
@@ -74,6 +107,7 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
for (const entry of entries) {
|
||||
plugin.managers.snapshot.add(entry);
|
||||
}
|
||||
|
||||
if (entries.length > 0) {
|
||||
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
|
||||
}
|
||||
@@ -95,18 +129,65 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: MVS
|
||||
}
|
||||
}
|
||||
|
||||
async function assignStateTransition(ctx: RuntimeContext, plugin: PluginContext, parentEntry: PluginStateSnapshotManager.Entry, parent: Snapshot, options: MVSLoadOptions, snapshotIndex: number, snapshotCount: number) {
|
||||
const transitions = await generateStateTransition(ctx, parent, snapshotIndex, snapshotCount);
|
||||
if (!transitions?.frames.length) return;
|
||||
|
||||
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
const animation: PluginState.StateTransition = {
|
||||
autoplay: !!transitions.tree.params?.autoplay,
|
||||
loop: !!transitions.tree.params?.loop,
|
||||
frames: [],
|
||||
};
|
||||
|
||||
for (let i = 0; i < transitions.frames.length; i++) {
|
||||
const frame = transitions.frames[i];
|
||||
const molstarTree = convertMvsToMolstar(frame[0], options.sourceUrl);
|
||||
const entry = molstarTreeToEntry(
|
||||
plugin,
|
||||
molstarTree,
|
||||
parent.animation,
|
||||
{ ...parent.metadata, previousTransitionDurationMs: transitions.frametimeMs },
|
||||
options
|
||||
);
|
||||
|
||||
StateTree.reuseTransformParams(entry.snapshot.data!.tree, parentEntry.snapshot.data!.tree);
|
||||
|
||||
animation.frames.push({
|
||||
durationInMs: frame[1],
|
||||
data: entry.snapshot.data!,
|
||||
camera: transitions.tree.params?.include_camera ? entry.snapshot.camera : undefined,
|
||||
canvas3d: transitions.tree.params?.include_canvas ? entry.snapshot.canvas3d : undefined,
|
||||
});
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: `Loading animation for snapshot ${snapshotIndex + 1}/${snapshotCount}...`, current: i + 1, max: transitions.frames.length });
|
||||
}
|
||||
}
|
||||
|
||||
parentEntry.snapshot.transition = animation;
|
||||
}
|
||||
|
||||
function molstarTreeToEntry(
|
||||
plugin: PluginContext,
|
||||
tree: MolstarTree,
|
||||
animation: MVSAnimationNode<'animation'> | undefined,
|
||||
metadata: SnapshotMetadata & { previousTransitionDurationMs?: number },
|
||||
options: { keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }
|
||||
) {
|
||||
const context = MolstarLoadingContext.create();
|
||||
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, { replaceExisting: true, extensions: options?.extensions ?? BuiltinLoadingExtensions });
|
||||
snapshot.canvas3d = {
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
|
||||
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas, animation) : undefined,
|
||||
};
|
||||
if (!options?.keepCamera) {
|
||||
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
|
||||
}
|
||||
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
|
||||
|
||||
if (tree.custom?.molstar_on_load_markdown_commands) {
|
||||
snapshot.onLoadMarkdownCommands = tree.custom.molstar_on_load_markdown_commands;
|
||||
}
|
||||
|
||||
const entryParams: PluginStateSnapshotManager.EntryParams = {
|
||||
key: metadata.key,
|
||||
name: metadata.title,
|
||||
@@ -127,7 +208,7 @@ export interface MolstarLoadingContext {
|
||||
cameraParams?: MolstarNodeParams<'camera'>,
|
||||
focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[],
|
||||
},
|
||||
canvas?: MolstarNodeParams<'canvas'>,
|
||||
canvas?: MolstarNode<'canvas'>,
|
||||
}
|
||||
export const MolstarLoadingContext = {
|
||||
create(): MolstarLoadingContext {
|
||||
@@ -153,31 +234,112 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
},
|
||||
parse(updateParent: UpdateTarget, node: MolstarNode<'parse'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
} else if (format === 'pdb') {
|
||||
return updateParent;
|
||||
} else if (format === 'map') {
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
case 'gro':
|
||||
case 'xyz':
|
||||
case 'mol':
|
||||
case 'sdf':
|
||||
case 'mol2':
|
||||
case 'xtc':
|
||||
case 'lammpstrj':
|
||||
case 'dcd':
|
||||
case 'nctraj':
|
||||
case 'trr':
|
||||
return updateParent;
|
||||
case 'psf':
|
||||
return UpdateTarget.apply(updateParent, ParsePsf, {});
|
||||
case 'prmtop':
|
||||
return UpdateTarget.apply(updateParent, ParsePrmtop, {});
|
||||
case 'top':
|
||||
return UpdateTarget.apply(updateParent, ParseTop, {});
|
||||
case 'map':
|
||||
return UpdateTarget.apply(updateParent, ParseCcp4, {});
|
||||
case 'dx':
|
||||
case 'dxbin':
|
||||
return UpdateTarget.apply(updateParent, ParseDx, {});
|
||||
default:
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
coordinates(updateParent: UpdateTarget, node: MolstarNode<'coordinates'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'nctraj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromNctraj);
|
||||
case 'dcd':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromDcd);
|
||||
case 'trr':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromTrr);
|
||||
case 'xtc':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromXtc);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, CoordinatesFromLammpstraj);
|
||||
default:
|
||||
console.error(`Unknown format in "coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory(updateParent: UpdateTarget, node: MolstarNode<'trajectory'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
} else if (format === 'pdb') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
switch (format) {
|
||||
case 'cif':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
case 'pdb':
|
||||
case 'pdbqt':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, { isPdbqt: format === 'pdbqt' });
|
||||
case 'gro':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromGRO);
|
||||
case 'xyz':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromXYZ);
|
||||
case 'mol':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL);
|
||||
case 'sdf':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromSDF);
|
||||
case 'mol2':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMOL2);
|
||||
case 'lammpstrj':
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromLammpsTrajData);
|
||||
default:
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'trajectory_with_coordinates'>): UpdateTarget | undefined {
|
||||
const result = UpdateTarget.apply(updateParent, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
topology_with_coordinates(updateParent: UpdateTarget, node: MolstarNode<'topology_with_coordinates'>): UpdateTarget | undefined {
|
||||
let parsed: UpdateTarget;
|
||||
const format = node.params.format;
|
||||
switch (format) {
|
||||
case 'psf':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPsf, {});
|
||||
break;
|
||||
case 'prmtop':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromPrmtop, {});
|
||||
break;
|
||||
case 'top':
|
||||
parsed = UpdateTarget.apply(updateParent, TopologyFromTop, {});
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown format in "topology_with_coordinates" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
const result = UpdateTarget.apply(parsed, MVSTrajectoryWithCoordinates, {
|
||||
coordinatesRef: node.params.coordinates_ref,
|
||||
});
|
||||
return UpdateTarget.setMvsDependencies(result, [node.params.coordinates_ref]);
|
||||
},
|
||||
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const annotations = collectAnnotationReferences(node, context);
|
||||
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
|
||||
@@ -198,24 +360,19 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
structure(updateParent: UpdateTarget, node: MolstarSubtree<'structure'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const props = structureProps(node);
|
||||
const struct = UpdateTarget.apply(updateParent, StructureFromModel, props);
|
||||
let transformed = struct;
|
||||
for (const t of transformProps(node)) {
|
||||
transformed = UpdateTarget.apply(transformed, TransformStructureConformation, t); // applying to the result of previous transform, to get the correct transform order
|
||||
}
|
||||
const transformed = transformAndInstantiateStructure(struct, node);
|
||||
const annotationTooltips = collectAnnotationTooltips(node, context);
|
||||
const inlineTooltips = collectInlineTooltips(node, context);
|
||||
if (annotationTooltips.length + inlineTooltips.length > 0) {
|
||||
UpdateTarget.apply(struct, CustomStructureProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
|
||||
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationTooltipsProvider.descriptor.name,
|
||||
CustomTooltipsProvider.descriptor.name,
|
||||
],
|
||||
});
|
||||
}
|
||||
UpdateTarget.apply(struct, CustomStructureProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
|
||||
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationTooltipsProvider.descriptor.name,
|
||||
CustomTooltipsProvider.descriptor.name,
|
||||
],
|
||||
}); // CustomStructureProperties must be applied even when `annotationTooltips` and `inlineTooltips` are empty, otherwise tooltips would persists across MVS snapshots
|
||||
const inlineLabels = collectInlineLabels(node, context);
|
||||
if (inlineLabels.length > 0) {
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
@@ -227,7 +384,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
colorTheme: colorThemeForNode(nearestReprNode, context),
|
||||
});
|
||||
}
|
||||
return struct;
|
||||
return transformed;
|
||||
},
|
||||
tooltip: undefined, // No action needed, already loaded in `structure`
|
||||
tooltip_from_uri: undefined, // No action needed, already loaded in `structure`
|
||||
@@ -237,21 +394,21 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
return updateParent;
|
||||
}
|
||||
const selector = node.params.selector;
|
||||
return UpdateTarget.apply(updateParent, StructureComponent, {
|
||||
return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, StructureComponent, {
|
||||
type: componentPropsFromSelector(selector),
|
||||
label: prettyNameFromSelector(selector),
|
||||
nullIfEmpty: false,
|
||||
});
|
||||
}), node);
|
||||
},
|
||||
component_from_uri(updateParent: UpdateTarget, node: MolstarSubtree<'component_from_uri'>, context: MolstarLoadingContext): UpdateTarget | undefined {
|
||||
if (isPhantomComponent(node)) return undefined;
|
||||
const props = componentFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
|
||||
return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props), node);
|
||||
},
|
||||
component_from_source(updateParent: UpdateTarget, node: MolstarSubtree<'component_from_source'>, context: MolstarLoadingContext): UpdateTarget | undefined {
|
||||
if (isPhantomComponent(node)) return undefined;
|
||||
const props = componentFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
|
||||
return transformAndInstantiateStructure(UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props), node);
|
||||
},
|
||||
representation(updateParent: UpdateTarget, node: MolstarNode<'representation'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
return UpdateTarget.apply(updateParent, StructureRepresentation3D, {
|
||||
@@ -260,14 +417,18 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
});
|
||||
},
|
||||
volume(updateParent: UpdateTarget, node: MolstarNode<'volume'>): UpdateTarget | undefined {
|
||||
let volume: UpdateTarget;
|
||||
if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Ccp4)) {
|
||||
return UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromCcp4, {});
|
||||
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Dx)) {
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromDx, {});
|
||||
} else if (updateParent.transformer?.definition.to.includes(PluginStateObject.Format.Cif)) {
|
||||
return UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
|
||||
volume = UpdateTarget.apply(updateParent, VolumeFromDensityServerCif, { blockHeader: node.params.channel_id || undefined });
|
||||
} else {
|
||||
console.error(`Unsupported volume format`);
|
||||
return undefined;
|
||||
}
|
||||
return transformAndInstantiateVolume(volume, node);
|
||||
},
|
||||
volume_representation(updateParent: UpdateTarget, node: MolstarNode<'volume_representation'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
return UpdateTarget.apply(updateParent, VolumeRepresentation3D, {
|
||||
@@ -296,27 +457,32 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
|
||||
return updateParent;
|
||||
},
|
||||
canvas(updateParent: UpdateTarget, node: MolstarNode<'canvas'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.canvas = node.params;
|
||||
context.canvas = node;
|
||||
return updateParent;
|
||||
},
|
||||
primitives(updateParent: UpdateTarget, tree: MolstarSubtree<'primitives'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const refs = getPrimitiveStructureRefs(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree });
|
||||
return applyPrimitiveVisuals(data, refs);
|
||||
const clip = clippingForNode(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree as any });
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const refs = new Set(tree.params.references);
|
||||
const clip = clippingForNode(tree);
|
||||
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
|
||||
return applyPrimitiveVisuals(data, new Set(tree.params.references));
|
||||
UpdateTarget.setMvsDependencies(data, refs); // MVSInlinePrimitiveData must depend on `refs` because it caches positions
|
||||
return applyPrimitiveVisuals(data, refs, clip);
|
||||
},
|
||||
};
|
||||
|
||||
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>) {
|
||||
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh' }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(mesh, ShapeRepresentation3D);
|
||||
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels' }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(labels, ShapeRepresentation3D);
|
||||
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines' }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(lines, ShapeRepresentation3D);
|
||||
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>, clip: Clip.Props | undefined) {
|
||||
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(mesh, MVSShapeRepresentation3D);
|
||||
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(labels, MVSShapeRepresentation3D);
|
||||
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines', clip }, { state: { isGhost: true } }), refs);
|
||||
UpdateTarget.apply(lines, MVSShapeRepresentation3D);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-schema';
|
||||
import { treeValidationIssues } from './tree/generic/tree-validation';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { MVSAnimationSchema, MVSAnimationTree } from './tree/animation/animation-tree';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
@@ -53,6 +55,8 @@ export interface Snapshot {
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: SnapshotMetadata,
|
||||
/** Optional animation */
|
||||
animation?: MVSAnimationTree,
|
||||
}
|
||||
|
||||
/** MVSData with a single state */
|
||||
@@ -189,7 +193,14 @@ function majorVersion(semanticVersion: string | number): number | undefined {
|
||||
|
||||
function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (snapshot.root === undefined) return [`"root" missing in snapshot`];
|
||||
return treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const state = treeValidationIssues(MVSTreeSchema, snapshot.root, options);
|
||||
const animation = 'animation' in snapshot && snapshot.animation !== undefined
|
||||
? treeValidationIssues(MVSAnimationSchema, snapshot.animation, options)
|
||||
: undefined;
|
||||
if (state && animation) return [...state, ...animation];
|
||||
if (state) return state;
|
||||
if (animation) return animation;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
|
||||
195
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
195
src/extensions/mvs/tree/animation/animation-tree.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, list, OptionalField, RequiredField, str, union, nullable, literal, ValueFor, dict } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { ColorT, ContinuousPalette, DiscretePalette, Matrix, Vector3 } from '../mvs/param-types';
|
||||
|
||||
type Easing =
|
||||
| 'linear'
|
||||
| 'bounce-in' | 'bounce-out' | 'bounce-in-out'
|
||||
| 'circle-in' | 'circle-out' | 'circle-in-out'
|
||||
| 'cubic-in' | 'cubic-out' | 'cubic-in-out'
|
||||
| 'exp-in' | 'exp-out' | 'exp-in-out'
|
||||
| 'quad-in' | 'quad-out' | 'quad-in-out'
|
||||
| 'sin-in' | 'sin-out' | 'sin-in-out'
|
||||
const Easing = literal<Easing>(
|
||||
'linear',
|
||||
'bounce-in', 'bounce-out', 'bounce-in-out',
|
||||
'circle-in', 'circle-out', 'circle-in-out',
|
||||
'cubic-in', 'cubic-out', 'cubic-in-out',
|
||||
'exp-in', 'exp-out', 'exp-in-out',
|
||||
'quad-in', 'quad-out', 'quad-in-out',
|
||||
'sin-in', 'sin-out', 'sin-in-out',
|
||||
);
|
||||
|
||||
export type MVSAnimationEasing = ValueFor<typeof Easing>;
|
||||
|
||||
const _Noise = {
|
||||
/** Magnitude of the noise to apply to the interpolated value. */
|
||||
noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the interpolated value.')
|
||||
// support cummulative noise?
|
||||
};
|
||||
|
||||
const _Common = {
|
||||
/** Reference to the node. */
|
||||
target_ref: RequiredField(str, 'Reference to the node.'),
|
||||
/** Value accessor. */
|
||||
property: RequiredField(union(str, list(union(str, int))), 'Value accessor.'),
|
||||
/** Start time of the transition in milliseconds. */
|
||||
start_ms: OptionalField(float, 0, 'Start time of the transition in milliseconds.'),
|
||||
/** Duration of the transition in milliseconds. */
|
||||
duration_ms: RequiredField(float, 'Duration of the transition in milliseconds.'),
|
||||
};
|
||||
|
||||
const _Frequency = {
|
||||
/** Determines how many times the interpolation loops. Current T = frequency * t mod 1. */
|
||||
frequency: OptionalField(int, 1, 'Determines how many times the interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
const _Easing = {
|
||||
/** Easing function to use for the transition. */
|
||||
easing: OptionalField(Easing, 'linear', 'Easing function to use for the transition.'),
|
||||
};
|
||||
|
||||
const ScalarInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used. */
|
||||
start: OptionalField(nullable(union(float, list(float))), null, 'Start value. If a list of values is provided, each element will be interpolated separately. If unset, parent state value is used.'),
|
||||
/** End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(union(float, list(float))), null, 'End value. If a list of values is provided, each element will be interpolated separately. If unset, only noise is applied.'),
|
||||
/** Whether to round the values to the closest integer. Useful for example for trajectory animation. */
|
||||
discrete: OptionalField(bool, false, 'Whether to round the values to the closest integer. Useful for example for trajectory animation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const Vec3Interpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). */
|
||||
start: OptionalField(nullable(list(float)), null, 'Start value. If unset, parent state value is used. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...).'),
|
||||
/** End value. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(list(float)), null, 'End value. Must be array of length 3N (x1, y1, z1, x2, y2, z2, ...). If unset, only noise is applied.'),
|
||||
/** Whether to use spherical interpolation. */
|
||||
spherical: OptionalField(bool, false, 'Whether to use spherical interpolation.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const RotationMatrixInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. */
|
||||
start: OptionalField(nullable(Matrix), null, 'Start value. If unset, parent state value is used.'),
|
||||
/** End value. If unset, only noise is applied. */
|
||||
end: OptionalField(nullable(Matrix), null, 'End value. If unset, only noise is applied.'),
|
||||
..._Noise,
|
||||
};
|
||||
|
||||
const ColorInterpolation = {
|
||||
..._Common,
|
||||
..._Frequency,
|
||||
..._Easing,
|
||||
/** Start value. If unset, parent state value is used. */
|
||||
start: OptionalField(union(nullable(ColorT), dict(union(int, str), ColorT)), null, 'Start value. If unset, parent state value is used.'),
|
||||
/** End value. */
|
||||
end: OptionalField(union(nullable(ColorT), dict(union(int, str), ColorT)), null, 'End value.'),
|
||||
/** Palette to sample colors from. Overrides start and end values. */
|
||||
palette: OptionalField(nullable(union(DiscretePalette, ContinuousPalette)), null, 'Palette to sample colors from. Overrides start and end values.'),
|
||||
};
|
||||
|
||||
const TransformationMatrixInterpolation = {
|
||||
..._Common,
|
||||
/** Pivot point for rotation and scale. */
|
||||
pivot: OptionalField(nullable(Vector3), null, 'Pivot point for rotation and scale.'),
|
||||
/** Start rotation value. If unset, parent state value is used. */
|
||||
rotation_start: OptionalField(nullable(Matrix), null, 'Start rotation value. If unset, parent state value is used.'),
|
||||
/** End rotation value. If unset, only noise is applied */
|
||||
rotation_end: OptionalField(nullable(Matrix), null, 'End rotation value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the rotation. */
|
||||
rotation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the rotation.'),
|
||||
/** Easing function to use for the rotation. */
|
||||
rotation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the rotation.'),
|
||||
/** Determines how many times the rotation interpolation loops. Current T = frequency * t mod 1. */
|
||||
rotation_frequency: OptionalField(int, 1, 'Determines how many times the rotation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
rotation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
/** Start translation value. If unset, parent state value is used. */
|
||||
translation_start: OptionalField(nullable(Vector3), null, 'Start translation value. If unset, parent state value is used.'),
|
||||
/** End translation value. If unset, only noise is applied. */
|
||||
translation_end: OptionalField(nullable(Vector3), null, 'End translation value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the translation. */
|
||||
translation_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the translation.'),
|
||||
/** Easing function to use for the translation. */
|
||||
translation_easing: OptionalField(Easing, 'linear', 'Easing function to use for the translation.'),
|
||||
/** Determines how many times the translation interpolation loops. Current T = frequency * t mod 1. */
|
||||
translation_frequency: OptionalField(int, 1, 'Determines how many times the translation interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
translation_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
/** Start scale value. If unset, parent state value is used. */
|
||||
scale_start: OptionalField(nullable(Vector3), null, 'Start scale value. If unset, parent state value is used.'),
|
||||
/** End scale value. If unset, only noise is applied. */
|
||||
scale_end: OptionalField(nullable(Vector3), null, 'End scale value. If unset, only noise is applied.'),
|
||||
/** Magnitude of the noise to apply to the scale. */
|
||||
scale_noise_magnitude: OptionalField(float, 0, 'Magnitude of the noise to apply to the scale.'),
|
||||
/** Easing function to use for the scale. */
|
||||
scale_easing: OptionalField(Easing, 'linear', 'Easing function to use for the scale.'),
|
||||
/** Determines how many times the scale interpolation loops. Current T = frequency * t mod 1. */
|
||||
scale_frequency: OptionalField(int, 1, 'Determines how many times the scale interpolation loops. Current T = frequency * t mod 1.'),
|
||||
/** Whether to alternate the direction of the interpolation for frequency > 1. */
|
||||
scale_alternate_direction: OptionalField(bool, false, 'Whether to alternate the direction of the interpolation for frequency > 1.'),
|
||||
};
|
||||
|
||||
export const MVSAnimationSchema = TreeSchema({
|
||||
rootKind: 'animation',
|
||||
nodes: {
|
||||
animation: {
|
||||
description: 'Animation root node',
|
||||
parent: [],
|
||||
params: SimpleParamsSchema({
|
||||
/** Frame time in milliseconds. */
|
||||
frame_time_ms: OptionalField(float, 1000 / 60, 'Frame time in milliseconds.'),
|
||||
/** Total duration of the animation. If not specified, computed as maximum of all transitions. */
|
||||
duration_ms: OptionalField(nullable(float), null, 'Total duration of the animation. If not specified, computed as maximum of all transitions.'),
|
||||
/** Determines whether the animation should autoplay when a snapshot is loaded */
|
||||
autoplay: OptionalField(bool, true, 'Determines whether the animation should autoplay when a snapshot is loaded.'),
|
||||
/** Determines whether the animation should loop when it reaches the end. */
|
||||
loop: OptionalField(bool, false, 'Determines whether the animation should loop when it reaches the end.'),
|
||||
/** Determines whether the camera state should be included in the animation. */
|
||||
include_camera: OptionalField(bool, false, 'Determines whether the camera state should be included in the animation.'),
|
||||
/** Determines whether the canvas state should be included in the animation. */
|
||||
include_canvas: OptionalField(bool, false, 'Determines whether the canvas state should be included in the animation.'),
|
||||
}),
|
||||
},
|
||||
interpolate: {
|
||||
description: 'This node enables interpolating between values',
|
||||
parent: ['animation'],
|
||||
params: UnionParamsSchema(
|
||||
'kind',
|
||||
'Interpolation kind',
|
||||
{
|
||||
scalar: SimpleParamsSchema(ScalarInterpolation),
|
||||
vec3: SimpleParamsSchema(Vec3Interpolation),
|
||||
rotation_matrix: SimpleParamsSchema(RotationMatrixInterpolation),
|
||||
transform_matrix: SimpleParamsSchema(TransformationMatrixInterpolation),
|
||||
color: SimpleParamsSchema(ColorInterpolation),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export type MVSAnimationKind = keyof typeof MVSAnimationSchema.nodes
|
||||
export type MVSAnimationNode<TKind extends MVSAnimationKind = MVSAnimationKind> = NodeFor<typeof MVSAnimationSchema, TKind>
|
||||
export type MVSAnimationTree = TreeFor<typeof MVSAnimationSchema>
|
||||
export type MVSAnimationNodeParams<TKind extends MVSAnimationKind> = ParamsOfKind<MVSAnimationTree, TKind>
|
||||
export type MVSAnimationSubtree<TKind extends MVSAnimationKind = MVSAnimationKind> = SubtreeOfKind<MVSAnimationTree, TKind>
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
@@ -48,7 +48,7 @@ describe('fieldValidationIssues', () => {
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues union', async () => {
|
||||
const stringOrNumberParam = RequiredField(union([str, float]), 'Testing required field stringOrNumberParam');
|
||||
const stringOrNumberParam = RequiredField(union(str, float), 'Testing required field stringOrNumberParam');
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/PathReporter';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
|
||||
@@ -26,34 +24,83 @@ export const bool = iots.boolean;
|
||||
export const tuple = iots.tuple;
|
||||
/** Type definition for a list/array, e.g. `list(str)` */
|
||||
export const list = iots.array;
|
||||
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
|
||||
export const union = iots.union;
|
||||
/** Type definition used to create objects */
|
||||
export const obj = iots.type;
|
||||
/** Type definition used to create partial objects */
|
||||
export const partial = iots.partial;
|
||||
/** Type definition for a dictionary/mapping/record, e.g. `dict(str, float)` means type `{ [K in string]: number }` */
|
||||
export const dict = iots.record;
|
||||
|
||||
/** Type definition used to create objects, e.g. `object({ name: str, age: float }, { address: str })` means type `{ name: string, age: number, address?: string }` */
|
||||
export function object<P extends iots.Props, Q extends iots.Props>(props: P, optionalProps: undefined, name?: string): iots.TypeC<P>;
|
||||
export function object<P extends iots.Props, Q extends iots.Props>(props: P, optionalProps: Q, name?: string): iots.IntersectionC<[iots.TypeC<P>, iots.PartialC<Q>]>;
|
||||
export function object<P extends iots.Props, Q extends iots.Props>(props: P, optionalProps?: Q, name?: string) {
|
||||
if (!optionalProps) {
|
||||
return iots.type(props, name);
|
||||
}
|
||||
|
||||
if (name === undefined) {
|
||||
const nameChunks = [];
|
||||
for (const key in props) {
|
||||
nameChunks.push(`${key}: ${props[key].name}`);
|
||||
}
|
||||
for (const key in optionalProps) {
|
||||
nameChunks.push(`${key}?: ${optionalProps[key].name}`);
|
||||
}
|
||||
name = `{ ${nameChunks.join(', ')} }`;
|
||||
}
|
||||
return iots.intersection([iots.type(props), iots.partial(optionalProps)], name);
|
||||
}
|
||||
|
||||
/** Type definition used to create partial objects, e.g. `partial({ name: str, age: float })` means type `{ name?: string, age?: number }` */
|
||||
export function partial<P extends iots.Props>(props: P, name?: string) {
|
||||
if (name === undefined) {
|
||||
const nameChunks = [];
|
||||
for (const key in props) {
|
||||
nameChunks.push(`${key}?: ${props[key].name}`);
|
||||
}
|
||||
name = `{ ${nameChunks.join(', ')} }`;
|
||||
}
|
||||
return iots.partial(props, name);
|
||||
}
|
||||
|
||||
/** Type definition for union types, e.g. `union(str, int)` means string or integer */
|
||||
export function union<T1 extends iots.Mixed, T2 extends iots.Mixed, TOthers extends iots.Mixed[]>(first: T1, second: T2, ...others: TOthers): iots.UnionC<[T1, T2, ...TOthers]> {
|
||||
const baseTypes: iots.Mixed[] = [];
|
||||
for (const type of [first, second, ...others]) {
|
||||
if (type instanceof iots.UnionType) {
|
||||
baseTypes.push(...type.types);
|
||||
} else {
|
||||
baseTypes.push(type);
|
||||
}
|
||||
}
|
||||
return iots.union(baseTypes as any);
|
||||
}
|
||||
|
||||
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
|
||||
export function nullable<T extends iots.Type<any>>(type: T) {
|
||||
return union([type, iots.null]);
|
||||
export function nullable<V>(type: iots.Type<V>): iots.Type<V | null> {
|
||||
return union(type, iots.null);
|
||||
}
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
|
||||
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue'.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* export type MyColor = 'red' | 'green' | 'blue';
|
||||
* export const MyColor = literal<MyColor>('red', 'green', 'blue');
|
||||
* ```
|
||||
*
|
||||
* (it looks stupid to repeat the list of values but it will result in nicer type bundle (for MolViewStories))
|
||||
*/
|
||||
export function literal<V extends string | number | boolean>(...values: V[]) {
|
||||
if (values.length === 0) {
|
||||
throw new Error(`literal type must have at least one value`);
|
||||
}
|
||||
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
|
||||
const typeName = values.length === 1 ? onelinerJsonString(values[0]) : `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
|
||||
const valueSet = new Set(values);
|
||||
return new iots.Type<V>(
|
||||
typeName,
|
||||
((value: any) => values.includes(value)) as any,
|
||||
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
|
||||
((value: any) => valueSet.has(value)) as any,
|
||||
(value, ctx) => valueSet.has(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
|
||||
value => value
|
||||
);
|
||||
}
|
||||
/** Type definition for mapping between two types, e.g. `mapping(str, float)` means type `{ [key in string]: number }` */
|
||||
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
|
||||
return iots.record(from, to);
|
||||
}
|
||||
|
||||
|
||||
interface FieldBase<V extends AllowedValueTypes = any, R extends boolean = boolean> {
|
||||
@@ -102,6 +149,41 @@ export function fieldValidationIssues<F extends Field>(field: F, value: any): st
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined;
|
||||
} else {
|
||||
return PathReporter.report(validation);
|
||||
return reportErrors(validation.left);
|
||||
}
|
||||
}
|
||||
|
||||
// Inlining `reportErrors` instead of `import { PathReporter } from 'io-ts/PathReporter'`;
|
||||
// because it breaks Deno usage.
|
||||
|
||||
function reportErrors(errors: iots.Errors): string[] | undefined {
|
||||
if (errors.length === 0) return undefined;
|
||||
return errors.map(getMessage);
|
||||
}
|
||||
|
||||
function getMessage(e: iots.ValidationError) {
|
||||
return e.message !== undefined
|
||||
? e.message
|
||||
: `Invalid value ${stringifyError(e.value)} supplied to ${getContextPath(e.context)}`;
|
||||
}
|
||||
|
||||
function getContextPath(context: iots.ValidationError['context']) {
|
||||
return context.map(a => `${a.key}: ${a.type.name}`).join('/');
|
||||
}
|
||||
|
||||
function getFunctionName(f: Function & { displayName?: string }) {
|
||||
return f.displayName || f.name || `<function ${f.length}>`;
|
||||
}
|
||||
|
||||
function stringifyError(v: any) {
|
||||
if (typeof v === 'function') {
|
||||
return getFunctionName(v);
|
||||
}
|
||||
if (typeof v === 'number' && !isFinite(v)) {
|
||||
if (isNaN(v)) {
|
||||
return 'NaN';
|
||||
}
|
||||
return v > 0 ? 'Infinity' : '-Infinity';
|
||||
}
|
||||
return JSON.stringify(v);
|
||||
}
|
||||
@@ -4,12 +4,8 @@
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { Field } from './field-schema';
|
||||
import { AllRequired, ParamsSchema, SimpleParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
import { mapObjectMap } from '../../../../mol-util/object';
|
||||
import { AllRequired, ParamsSchema, ValuesFor } from './params-schema';
|
||||
|
||||
/** Type of "custom" of a tree node (key-value storage with arbitrary JSONable values) */
|
||||
export type CustomProps = Partial<Record<string, any>>
|
||||
@@ -114,120 +110,3 @@ export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSc
|
||||
|
||||
/** Type of tree which conforms to tree schema `TTreeSchema` */
|
||||
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
|
||||
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
* If `options.anyRoot` is true, the kind of the root node is not enforced.
|
||||
*/
|
||||
export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: { requireAll?: boolean, noExtra?: boolean, anyRoot?: boolean, parent?: string } = {}): string[] | undefined {
|
||||
if (!isPlainObject(tree)) return [`Node must be an object, not ${tree}`];
|
||||
if (!options.anyRoot && tree.kind !== schema.rootKind) return [`Invalid root node kind "${tree.kind}", root must be of kind "${schema.rootKind}"`];
|
||||
const nodeSchema = schema.nodes[tree.kind];
|
||||
if (!nodeSchema) return [`Unknown node kind "${tree.kind}"`];
|
||||
if (nodeSchema.parent && (options.parent !== undefined) && !nodeSchema.parent.includes(options.parent)) {
|
||||
return [`Node of kind "${tree.kind}" cannot appear as a child of "${options.parent}". Allowed parents for "${tree.kind}" are: ${nodeSchema.parent.map(s => `"${s}"`).join(', ')}`];
|
||||
}
|
||||
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
|
||||
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
|
||||
if (tree.custom !== undefined && (typeof tree.custom !== 'object' || tree.custom === null)) {
|
||||
return [`Invalid "custom" for node of kind "${tree.kind}": must be an object, not ${tree.custom}.`];
|
||||
}
|
||||
for (const child of getChildren(tree)) {
|
||||
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
|
||||
if (issues) return issues;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validate a tree against the given schema.
|
||||
* Do nothing if OK; print validation issues on console and throw an error is the tree does not conform.
|
||||
* Include `label` in the printed output. */
|
||||
export function validateTree(schema: TreeSchema, tree: Tree, label: string): void {
|
||||
const issues = treeValidationIssues(schema, tree, { noExtra: true });
|
||||
if (issues) {
|
||||
console.warn(`Invalid ${label} tree:\n${treeToString(tree)}`);
|
||||
console.error(`${label} tree validation issues:`);
|
||||
for (const line of issues) {
|
||||
console.error(' ', line);
|
||||
}
|
||||
throw new Error('FormatError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
const h1 = markdown ? '## ' : ' - ';
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const h3 = markdown ? ' - ' : ' - ';
|
||||
const p3 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
const { description, params, parent } = schema.nodes[kind];
|
||||
out.push(`${h1}${code(kind)}`);
|
||||
if (kind === schema.rootKind) {
|
||||
out.push(`${p1}[Root of the tree must be of this kind]`);
|
||||
}
|
||||
if (description) {
|
||||
out.push(`${p1}${description}`);
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
if (params.type === 'simple') {
|
||||
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
|
||||
} else {
|
||||
const key = params.discriminator;
|
||||
const casesStr = Object.keys(params.cases).join(' | ');
|
||||
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
|
||||
if (params.discriminatorDescription) {
|
||||
out.push(`${p2}${params.discriminatorDescription}`);
|
||||
}
|
||||
out.push(`${p2}[This parameter determines the rest of parameters]`);
|
||||
for (const case_ in params.cases) {
|
||||
const caseStr = `${params.discriminator}: "${case_}"`;
|
||||
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
|
||||
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
|
||||
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
|
||||
const { h, p, code, bold } = formatting;
|
||||
for (const key in params.fields) {
|
||||
const field = params.fields[key];
|
||||
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
|
||||
const defaultValue = field.required ? undefined : field.default;
|
||||
if (field.description) {
|
||||
out.push(`${p}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFieldType(field: Field): string {
|
||||
const typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
return typeString.slice(1, -1);
|
||||
} else {
|
||||
return typeString;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ export function copyTree<T extends Tree>(root: T): T {
|
||||
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
|
||||
* nodes of other kinds will just be copied. */
|
||||
export type ConversionRules<A extends Tree, B extends Tree> = {
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => Subtree<B>[]
|
||||
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => { subtree: Subtree<B>[] }
|
||||
};
|
||||
|
||||
/** Apply a set of conversion rules to a tree to change to a different schema. */
|
||||
@@ -94,12 +94,12 @@ export function convertTree<A extends Tree, B extends Tree>(root: A, conversions
|
||||
const mapping = new Map<Subtree<A>, Subtree<B>>();
|
||||
let convertedRoot: Subtree<B>;
|
||||
dfs<A>(root, (node, parent) => {
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => Subtree<B>[]) | undefined;
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => { subtree: Subtree<B>[] }) | undefined;
|
||||
if (conversion) {
|
||||
const convertidos = conversion(node, parent);
|
||||
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
const converted = conversion(node, parent);
|
||||
if (!parent && converted?.subtree.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
let convParent = parent ? mapping.get(parent) : undefined;
|
||||
for (const conv of convertidos) {
|
||||
for (const conv of converted.subtree) {
|
||||
if (convParent) {
|
||||
(convParent.children ??= []).push(conv);
|
||||
} else {
|
||||
@@ -153,12 +153,14 @@ export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema:
|
||||
type TTree = TreeFor<S>;
|
||||
const rules: ConversionRules<TTree, TTree> = {};
|
||||
for (const kind in treeSchema.nodes) {
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any];
|
||||
rules[kind as Kind<Subtree<TTree>>] = node => ({
|
||||
subtree: [{
|
||||
kind: node.kind,
|
||||
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
|
||||
custom: node.custom,
|
||||
ref: node.ref,
|
||||
} as Node as any]
|
||||
});
|
||||
}
|
||||
return convertTree(tree, rules) as any;
|
||||
}
|
||||
|
||||
125
src/extensions/mvs/tree/generic/tree-validation.ts
Normal file
125
src/extensions/mvs/tree/generic/tree-validation.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject } from '../../../../mol-util/object';
|
||||
import { Field } from './field-schema';
|
||||
import { SimpleParamsSchema, paramsValidationIssues } from './params-schema';
|
||||
import { getChildren, getParams, Tree, TreeSchema } from './tree-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
* If `options.anyRoot` is true, the kind of the root node is not enforced.
|
||||
*/
|
||||
export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: { requireAll?: boolean, noExtra?: boolean, anyRoot?: boolean, parent?: string } = {}): string[] | undefined {
|
||||
if (!isPlainObject(tree)) return [`Node must be an object, not ${tree}`];
|
||||
if (!options.anyRoot && tree.kind !== schema.rootKind) return [`Invalid root node kind "${tree.kind}", root must be of kind "${schema.rootKind}"`];
|
||||
const nodeSchema = schema.nodes[tree.kind];
|
||||
if (!nodeSchema) return [`Unknown node kind "${tree.kind}"`];
|
||||
if (nodeSchema.parent && (options.parent !== undefined) && !nodeSchema.parent.includes(options.parent)) {
|
||||
return [`Node of kind "${tree.kind}" cannot appear as a child of "${options.parent}". Allowed parents for "${tree.kind}" are: ${nodeSchema.parent.map(s => `"${s}"`).join(', ')}`];
|
||||
}
|
||||
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
|
||||
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
|
||||
if (tree.custom !== undefined && (typeof tree.custom !== 'object' || tree.custom === null)) {
|
||||
return [`Invalid "custom" for node of kind "${tree.kind}": must be an object, not ${tree.custom}.`];
|
||||
}
|
||||
for (const child of getChildren(tree)) {
|
||||
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
|
||||
if (issues) return issues;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validate a tree against the given schema.
|
||||
* Do nothing if OK; print validation issues on console and throw an error is the tree does not conform.
|
||||
* Include `label` in the printed output. */
|
||||
export function validateTree(schema: TreeSchema, tree: Tree, label: string, plugin: PluginContext): void {
|
||||
const issues = treeValidationIssues(schema, tree, { noExtra: true });
|
||||
if (issues) {
|
||||
console.warn(`Invalid ${label} tree:\n${treeToString(tree)}`);
|
||||
console.error(`${label} tree validation issues:`);
|
||||
plugin.log.error(`${label} tree validation issues:`);
|
||||
for (const line of issues) {
|
||||
console.error(' ', line);
|
||||
plugin.log.error(line);
|
||||
}
|
||||
throw new Error('FormatError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
|
||||
return treeSchemaToString_(schema, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
const h1 = markdown ? '## ' : ' - ';
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const h3 = markdown ? ' - ' : ' - ';
|
||||
const p3 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
const { description, params, parent } = schema.nodes[kind];
|
||||
out.push(`${h1}${code(kind)}`);
|
||||
if (kind === schema.rootKind) {
|
||||
out.push(`${p1}[Root of the tree must be of this kind]`);
|
||||
}
|
||||
if (description) {
|
||||
out.push(`${p1}${description}`);
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
if (params.type === 'simple') {
|
||||
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
|
||||
} else {
|
||||
const key = params.discriminator;
|
||||
const casesStr = Object.keys(params.cases).join(' | ');
|
||||
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
|
||||
if (params.discriminatorDescription) {
|
||||
out.push(`${p2}${params.discriminatorDescription}`);
|
||||
}
|
||||
out.push(`${p2}[This parameter determines the rest of parameters]`);
|
||||
for (const case_ in params.cases) {
|
||||
const caseStr = `${params.discriminator}: "${case_}"`;
|
||||
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
|
||||
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
|
||||
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
|
||||
const { h, p, code, bold } = formatting;
|
||||
for (const key in params.fields) {
|
||||
const field = params.fields[key];
|
||||
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
|
||||
const defaultValue = field.required ? undefined : field.default;
|
||||
if (field.description) {
|
||||
out.push(`${p}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function formatFieldType(field: Field): string {
|
||||
const typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
return typeString.slice(1, -1);
|
||||
} else {
|
||||
return typeString;
|
||||
}
|
||||
}
|
||||
@@ -15,37 +15,99 @@ import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
|
||||
/** Convert `format` parameter of `parse` node in `MolstarTree`
|
||||
* into `format` and `is_binary` parameters in `MolstarTree` */
|
||||
export const ParseFormatMvsToMolstar = {
|
||||
// trajectory
|
||||
mmcif: { format: 'cif', is_binary: false },
|
||||
bcif: { format: 'cif', is_binary: true },
|
||||
pdb: { format: 'pdb', is_binary: false },
|
||||
pdbqt: { format: 'pdbqt', is_binary: false },
|
||||
gro: { format: 'gro', is_binary: false },
|
||||
xyz: { format: 'xyz', is_binary: false },
|
||||
mol: { format: 'mol', is_binary: false },
|
||||
sdf: { format: 'sdf', is_binary: false },
|
||||
mol2: { format: 'mol2', is_binary: false },
|
||||
lammpstrj: { format: 'lammpstrj', is_binary: false },
|
||||
// coordinates
|
||||
xtc: { format: 'xtc', is_binary: true },
|
||||
nctraj: { format: 'nctraj', is_binary: true },
|
||||
dcd: { format: 'dcd', is_binary: true },
|
||||
trr: { format: 'trr', is_binary: true },
|
||||
// topology
|
||||
psf: { format: 'psf', is_binary: false },
|
||||
prmtop: { format: 'prmtop', is_binary: false },
|
||||
top: { format: 'top', is_binary: false },
|
||||
// maps
|
||||
map: { format: 'map', is_binary: true },
|
||||
dx: { format: 'dx', is_binary: false },
|
||||
dxbin: { format: 'dxbin', is_binary: true },
|
||||
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
|
||||
|
||||
|
||||
const TopologyFormats = new Set<ParseFormatT>(['psf', 'prmtop', 'top']);
|
||||
|
||||
/** Conversion rules for conversion from `MVSTree` (with all parameter values) to `MolstarTree` */
|
||||
const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
|
||||
'download': node => [],
|
||||
'download': node => ({ subtree: [] }),
|
||||
'parse': (node, parent) => {
|
||||
const { format, is_binary } = ParseFormatMvsToMolstar[node.params.format];
|
||||
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref };
|
||||
if (parent?.kind === 'download') {
|
||||
return [
|
||||
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
|
||||
convertedNode,
|
||||
] satisfies MolstarNode[];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
|
||||
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
} else {
|
||||
console.warn('"parse" node is not being converted, this is suspicious');
|
||||
return [convertedNode] satisfies MolstarNode[];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
}
|
||||
},
|
||||
'coordinates': (node, parent) => {
|
||||
if (parent?.kind !== 'parse') throw new Error(`Parent of "coordinates" must be "parse", not "${parent?.kind}".`);
|
||||
const { format } = ParseFormatMvsToMolstar[parent.params.format];
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'coordinates', params: { format }, custom: node.custom, ref: node.ref }
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
},
|
||||
'structure': (node, parent) => {
|
||||
if (parent?.kind !== 'parse') throw new Error(`Parent of "structure" must be "parse", not "${parent?.kind}".`);
|
||||
const { format } = ParseFormatMvsToMolstar[parent.params.format];
|
||||
return [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[];
|
||||
|
||||
if (TopologyFormats.has(parent.params.format)) {
|
||||
if (!node.params.coordinates_ref) {
|
||||
throw new Error(`"structure" node with topology format "${parent.params.format}" must have "coordinates_ref" parameter.`);
|
||||
}
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'topology_with_coordinates', params: { format, coordinates_ref: node.params.coordinates_ref } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
} else if (node.params.coordinates_ref) {
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: { model_index: 0 } },
|
||||
{ kind: 'trajectory_with_coordinates', params: { coordinates_ref: node.params.coordinates_ref } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
subtree: [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index', 'coordinates_ref']), custom: node.custom, ref: node.ref },
|
||||
] satisfies MolstarNode[]
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -70,10 +132,30 @@ function fileExtensionMatches(filename: string, extensions: (FileExtension | '*'
|
||||
}
|
||||
|
||||
const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> = {
|
||||
// trajectory
|
||||
mmcif: ['.cif', '.mmif'],
|
||||
bcif: ['.bcif'],
|
||||
pdb: ['.pdb', '.ent'],
|
||||
pdbqt: ['.pdbqt'],
|
||||
gro: ['.gro'],
|
||||
xyz: ['.xyz'],
|
||||
mol: ['.mol'],
|
||||
sdf: ['.sdf'],
|
||||
mol2: ['.mol2'],
|
||||
lammpstrj: ['.lammpstrj'],
|
||||
// coordinates
|
||||
xtc: ['.xtc'],
|
||||
nctraj: ['.nc', '.nctraj'],
|
||||
dcd: ['.dcd'],
|
||||
trr: ['.trr'],
|
||||
// topology
|
||||
psf: ['.psf'],
|
||||
prmtop: ['.prmtop', '.parm7'],
|
||||
top: ['.top'],
|
||||
// volumes
|
||||
map: ['.map', '.ccp4', '.mrc', '.mrcs'],
|
||||
dx: ['.dx'],
|
||||
dxbin: ['.dxbin'],
|
||||
};
|
||||
|
||||
/** Run some sanity check on a MVSTree. Return a list of potential problems (`undefined` if there are none) */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { RequiredField, bool } from '../generic/field-schema';
|
||||
import { RequiredField, bool, str } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
|
||||
@@ -21,12 +21,23 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
...FullMVSTreeSchema.nodes.download,
|
||||
params: SimpleParamsSchema({
|
||||
...FullMVSTreeSchema.nodes.download.params.fields,
|
||||
/** Specifies whether file is downloaded as bytes array or string */
|
||||
is_binary: RequiredField(bool, 'Specifies whether file is downloaded as bytes array or string'),
|
||||
}),
|
||||
},
|
||||
parse: {
|
||||
...FullMVSTreeSchema.nodes.parse,
|
||||
params: SimpleParamsSchema({
|
||||
/** File format */
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's CoordinatesFrom*. */
|
||||
coordinates: {
|
||||
description: "Auxiliary node corresponding to Molstar's CoordinatesFrom*.",
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
/** File format */
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
}),
|
||||
},
|
||||
@@ -35,14 +46,32 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
/** File format */
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory_with_coordinates: {
|
||||
description: 'Auxiliary node corresponding to assigning a separate coordinates to a trajectory.',
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Coordinates reference */
|
||||
coordinates_ref: RequiredField(str, 'Coordinates reference'),
|
||||
}),
|
||||
},
|
||||
topology_with_coordinates: {
|
||||
description: 'Auxiliary node corresponding to assigning a separate coordinates to a topology.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
format: RequiredField(MolstarParseFormatT, 'File format'),
|
||||
coordinates_ref: RequiredField(str, 'Coordinates reference'),
|
||||
}),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
|
||||
model: {
|
||||
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
|
||||
parent: ['trajectory'],
|
||||
parent: ['trajectory', 'trajectory_with_coordinates', 'topology_with_coordinates'],
|
||||
params: SimpleParamsSchema(
|
||||
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
|
||||
),
|
||||
@@ -52,7 +81,7 @@ export const MolstarTreeSchema = TreeSchema({
|
||||
...FullMVSTreeSchema.nodes.structure,
|
||||
parent: ['model'],
|
||||
params: SimpleParamsSchema(
|
||||
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
|
||||
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index', 'coordinates_ref'] as const)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { GlobalMetadata, MVSData_State, Snapshot, SnapshotMetadata } from '../../mvs-data';
|
||||
import { MVSAnimationNodeParams, MVSAnimationSubtree } from '../animation/animation-tree';
|
||||
import { CustomProps } from '../generic/tree-schema';
|
||||
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree } from './mvs-tree';
|
||||
import { ColorT, PrimitivePositionT } from './param-types';
|
||||
|
||||
|
||||
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
|
||||
@@ -50,6 +52,8 @@ class _Base<TKind extends MVSKind> {
|
||||
|
||||
/** MVS builder pointing to the 'root' node */
|
||||
export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
protected _animation: Animation | undefined = undefined;
|
||||
|
||||
constructor(params_: CustomAndRef) {
|
||||
const { custom, ref } = params_;
|
||||
const node: MVSNode<'root'> = { kind: 'root', custom, ref };
|
||||
@@ -69,6 +73,7 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
return {
|
||||
root: deepClone(this._node),
|
||||
metadata: { ...metadata },
|
||||
animation: this?._animation ? deepClone(this._animation.node) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -88,7 +93,44 @@ export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
|
||||
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
|
||||
primitivesFromUri = bindMethod(this, PrimitivesMixinImpl, 'primitivesFromUri');
|
||||
|
||||
animation(params: MVSAnimationNodeParams<'animation'> & CustomAndRef = {}): Animation {
|
||||
this._animation ??= new Animation(params);
|
||||
return this._animation;
|
||||
}
|
||||
|
||||
/** Modifies custom state of the root */
|
||||
extendRootCustomState(custom: Record<string, any>): this {
|
||||
this._node.custom = { ...this._node.custom, ...custom };
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export class Animation {
|
||||
private _node: MVSAnimationSubtree<'animation'>;
|
||||
constructor(
|
||||
parameters: MVSAnimationNodeParams<'animation'> & CustomAndRef
|
||||
) {
|
||||
this._node = {
|
||||
kind: 'animation',
|
||||
children: [],
|
||||
...splitParams<MVSAnimationNodeParams<'animation'>>(parameters),
|
||||
};
|
||||
}
|
||||
|
||||
get node(): MVSAnimationSubtree<'animation'> {
|
||||
return this._node;
|
||||
}
|
||||
|
||||
interpolate(params: MVSAnimationNodeParams<'interpolate'> & CustomAndRef): Animation {
|
||||
const node = {
|
||||
kind: 'interpolate',
|
||||
...splitParams<MVSAnimationNodeParams<'interpolate'>>(params)
|
||||
} as MVSAnimationSubtree<'interpolate'>;
|
||||
this._node.children!.push(node);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -103,10 +145,10 @@ export class Download extends _Base<'download'> {
|
||||
|
||||
/** Subsets of 'structure' node params which will be passed to individual builder functions. */
|
||||
const StructureParamsSubsets = {
|
||||
model: ['block_header', 'block_index', 'model_index'],
|
||||
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id'],
|
||||
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max'],
|
||||
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius'],
|
||||
model: ['block_header', 'block_index', 'model_index', 'coordinates_ref'],
|
||||
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id', 'coordinates_ref'],
|
||||
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max', 'coordinates_ref'],
|
||||
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius', 'coordinates_ref'],
|
||||
} satisfies { [kind in MVSNodeParams<'structure'>['type']]: (keyof MVSNodeParams<'structure'>)[] };
|
||||
|
||||
|
||||
@@ -156,11 +198,16 @@ export class Parse extends _Base<'parse'> {
|
||||
volume(params: MVSNodeParams<'volume'> & CustomAndRef = {}): Volume {
|
||||
return new Volume(this._root, this.addChild('volume', params));
|
||||
}
|
||||
/** Add a 'coordinates' node indicating the parsed data type */
|
||||
coordinates(params: MVSNodeParams<'coordinates'> & CustomAndRef = {}): Parse {
|
||||
this.addChild('coordinates', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'structure' node */
|
||||
export class Structure extends _Base<'structure'> implements PrimitivesMixin {
|
||||
export class Structure extends _Base<'structure'> implements PrimitivesMixin, TransformMixin {
|
||||
/** Add a 'component' node and return builder pointing to it. 'component' node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component(params: Partial<MVSNodeParams<'component'>> & CustomAndRef = {}): Component {
|
||||
const fullParams = { ...params, selector: params.selector ?? 'all' };
|
||||
@@ -194,21 +241,15 @@ export class Structure extends _Base<'structure'> implements PrimitivesMixin {
|
||||
this.addChild('tooltip_from_source', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'transform' node and return builder pointing back to the structure node. 'transform' node instructs to rotate and/or translate structure coordinates. */
|
||||
transform(params: MVSNodeParams<'transform'> & CustomAndRef = {}): Structure {
|
||||
if (params.rotation && params.rotation.length !== 9) {
|
||||
throw new Error('ValueError: `rotation` parameter must be an array of 9 numbers');
|
||||
}
|
||||
this.addChild('transform', params);
|
||||
return this;
|
||||
}
|
||||
transform = bindMethod(this, TransformMixinImpl, 'transform');
|
||||
instance = bindMethod(this, TransformMixinImpl, 'instance');
|
||||
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
|
||||
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
|
||||
primitivesFromUri = bindMethod(this, PrimitivesMixinImpl, 'primitivesFromUri');
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
|
||||
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> implements FocusMixin {
|
||||
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> implements FocusMixin, TransformMixin {
|
||||
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
|
||||
representation(params: Partial<MVSNodeParams<'representation'>> & CustomAndRef = {}): Representation {
|
||||
const fullParams: MVSNodeParams<'representation'> = { ...params, type: params.type ?? 'cartoon' };
|
||||
@@ -225,6 +266,8 @@ export class Component extends _Base<'component' | 'component_from_uri' | 'compo
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
transform = bindMethod(this, TransformMixinImpl, 'transform');
|
||||
instance = bindMethod(this, TransformMixinImpl, 'instance');
|
||||
}
|
||||
|
||||
|
||||
@@ -250,17 +293,26 @@ export class Representation extends _Base<'representation'> {
|
||||
this.addChild('opacity', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
|
||||
clip(params: MVSNodeParams<'clip'> & CustomAndRef): Representation {
|
||||
this.addChild('clip', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
|
||||
export class Volume extends _Base<'volume'> implements FocusMixin {
|
||||
export class Volume extends _Base<'volume'> implements FocusMixin, TransformMixin {
|
||||
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
|
||||
representation(params: Partial<MVSNodeParams<'volume_representation'>> & CustomAndRef = {}): VolumeRepresentation {
|
||||
const fullParams: MVSNodeParams<'volume_representation'> = { ...params, type: params.type ?? 'isosurface' };
|
||||
return new VolumeRepresentation(this._root, this.addChild('volume_representation', fullParams));
|
||||
representation(params?: MVSNodeParams<'volume_representation'> & CustomAndRef): VolumeRepresentation {
|
||||
if (!params) {
|
||||
params = { type: 'isosurface' };
|
||||
}
|
||||
return new VolumeRepresentation(this._root, this.addChild('volume_representation', params));
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
transform = bindMethod(this, TransformMixinImpl, 'transform');
|
||||
instance = bindMethod(this, TransformMixinImpl, 'instance');
|
||||
}
|
||||
|
||||
|
||||
@@ -277,6 +329,11 @@ export class VolumeRepresentation extends _Base<'volume_representation'> impleme
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
|
||||
clip(params: MVSNodeParams<'clip'> & CustomAndRef): VolumeRepresentation {
|
||||
this.addChild('clip', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -309,6 +366,11 @@ export class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
this.addChild('primitive', { kind: 'distance_measurement', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines an angle between vectors (b - a) and (c - b). */
|
||||
angle(params: MVSPrimitiveSubparams<'angle_measurement'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'angle_measurement', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a label. */
|
||||
label(params: MVSPrimitiveSubparams<'label'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'label', ...params });
|
||||
@@ -319,23 +381,46 @@ export class Primitives extends _Base<'primitives'> implements FocusMixin {
|
||||
this.addChild('primitive', { kind: 'ellipse', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines an ellipsoid */
|
||||
/** Defines an ellipsoid. */
|
||||
ellipsoid(params: MVSPrimitiveSubparams<'ellipsoid'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'ellipsoid', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a sphere (a special case of ellipsoid). */
|
||||
sphere(params: {
|
||||
center: PrimitivePositionT,
|
||||
radius?: number | null,
|
||||
radius_extent?: number | null,
|
||||
color?: ColorT | null,
|
||||
tooltip?: string | null,
|
||||
} & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'ellipsoid', ...params });
|
||||
return this;
|
||||
}
|
||||
/** Defines a box. */
|
||||
box(params: MVSPrimitiveSubparams<'box'> & CustomAndRef): Primitives {
|
||||
this.addChild('primitive', { kind: 'box', ...params });
|
||||
return this;
|
||||
}
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
|
||||
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
|
||||
clip(params: MVSNodeParams<'clip'> & CustomAndRef): Primitives {
|
||||
this.addChild('clip', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'primitives_from_uri' node */
|
||||
class PrimitivesFromUri extends _Base<'primitives_from_uri'> implements FocusMixin {
|
||||
focus = bindMethod(this, FocusMixinImpl, 'focus');
|
||||
|
||||
/** Add a 'clip' node and return builder pointing back to the representation node. 'clip' node instructs to apply clipping to a visual representation. */
|
||||
clip(params: MVSNodeParams<'clip'> & CustomAndRef): PrimitivesFromUri {
|
||||
this.addChild('clip', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -369,17 +454,48 @@ interface PrimitivesMixin {
|
||||
/** Allows the definition of a (group of) geometric primitives. You can add any number of primitives and then assign shared options (color, opacity etc.). */
|
||||
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef): Primitives,
|
||||
/** Allows the definition of a (group of) geometric primitives provided dynamically. */
|
||||
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri,
|
||||
primitivesFromUri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri,
|
||||
};
|
||||
class PrimitivesMixinImpl extends _Base<MVSKind> implements PrimitivesMixin {
|
||||
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef = {}): Primitives {
|
||||
return new Primitives(this._root, this.addChild('primitives', params));
|
||||
}
|
||||
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri {
|
||||
primitivesFromUri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri {
|
||||
return new PrimitivesFromUri(this._root, this.addChild('primitives_from_uri', params));
|
||||
}
|
||||
};
|
||||
|
||||
interface TransformMixin {
|
||||
/** Add a 'transform' node and return builder pointing back to this node. 'transform' node instructs to rotate and/or translate coordinates. */
|
||||
transform(params: MVSNodeParams<'transform'> & CustomAndRef): this
|
||||
/** Add an 'instance' node and return builder pointing back to this node. 'instance' node instructs to create a new instance of the object. */
|
||||
instance(params: MVSNodeParams<'instance'> & CustomAndRef): this
|
||||
};
|
||||
class TransformMixinImpl extends _Base<MVSKind> implements TransformMixin {
|
||||
transform(params: MVSNodeParams<'transform'> & CustomAndRef = {}): any {
|
||||
validateTransformParams(params);
|
||||
this.addChild('transform', params);
|
||||
return this;
|
||||
}
|
||||
|
||||
instance(params: MVSNodeParams<'instance'> & CustomAndRef = {}): any {
|
||||
validateTransformParams(params);
|
||||
this.addChild('instance', params);
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
function validateTransformParams(params: MVSNodeParams<'transform' | 'instance'> & CustomAndRef) {
|
||||
if (params.rotation && params.rotation.length !== 9) {
|
||||
throw new Error('ValueError: `rotation` parameter must be an array of 9 numbers');
|
||||
}
|
||||
if (params.matrix && params.matrix.length !== 16) {
|
||||
throw new Error('ValueError: `matrix` parameter must be an array of 16 numbers');
|
||||
}
|
||||
if (params.matrix && (params.translation || params.rotation)) {
|
||||
throw new Error('ValueError: `matrix` parameter cannot be used together with `translation` or `rotation` parameters');
|
||||
}
|
||||
}
|
||||
|
||||
/** Demonstration of usage of MVS builder */
|
||||
export function builderDemo() {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str, union } from '../generic/field-schema';
|
||||
import { bool, dict, float, int, nullable, OptionalField, RequiredField, str, union } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { ColorT, FloatList, IntList, PrimitivePositionT, Vector3 } from './param-types';
|
||||
|
||||
@@ -31,9 +31,9 @@ const MeshParams = {
|
||||
/** Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i). */
|
||||
triangle_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i).'),
|
||||
/** Assign a color to each group. Where not assigned, uses `color`. */
|
||||
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
group_colors: OptionalField(dict(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
|
||||
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
group_tooltips: OptionalField(dict(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
/** Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
|
||||
@@ -53,14 +53,14 @@ const LinesParams = {
|
||||
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
|
||||
/** 2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...). */
|
||||
indices: RequiredField(IntList, '2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...).'),
|
||||
/** Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i). */
|
||||
/** Assign a number to each line to group them. If not specified, each line is considered a separate group (line i = group i). */
|
||||
line_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i).'),
|
||||
/** Assign a color to each group. Where not assigned, uses `color`. */
|
||||
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
group_colors: OptionalField(dict(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
|
||||
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
|
||||
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
group_tooltips: OptionalField(dict(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
|
||||
/** Assign a line width to each group. Where not assigned, uses `width`. */
|
||||
group_widths: OptionalField(mapping(int, float), {}, 'Assign a line width to each group. Where not assigned, uses `width`.'),
|
||||
group_widths: OptionalField(dict(int, float), {}, 'Assign a line width to each group. Where not assigned, uses `width`.'),
|
||||
/** Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
|
||||
color: OptionalField(nullable(ColorT), null, 'Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
|
||||
/** Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
|
||||
@@ -209,9 +209,9 @@ const EllipsoidParams = {
|
||||
/** Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center. */
|
||||
minor_axis_endpoint: OptionalField(nullable(PrimitivePositionT), null, 'Minor axis endpoint. If specified, overrides minor axis to be minor_axis_endpoint - center.'),
|
||||
/** Radii of the ellipsoid along each axis. */
|
||||
radius: OptionalField(nullable(union([Vector3, float])), null, 'Radii of the ellipsoid along each axis.'),
|
||||
radius: OptionalField(nullable(union(Vector3, float)), null, 'Radii of the ellipsoid along each axis.'),
|
||||
/** Added to the radii of the ellipsoid along each axis. */
|
||||
radius_extent: OptionalField(nullable(union([Vector3, float])), null, 'Added to the radii of the ellipsoid along each axis.'),
|
||||
radius_extent: OptionalField(nullable(union(Vector3, float)), null, 'Added to the radii of the ellipsoid along each axis.'),
|
||||
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
|
||||
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
|
||||
};
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { bool, float, nullable, OptionalField } from '../generic/field-schema';
|
||||
import { bool, float, int, literal, nullable, OptionalField, RequiredField } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
|
||||
import { Matrix, Vector3 } from './param-types';
|
||||
|
||||
const Cartoon = {
|
||||
/** Scales the corresponding visuals */
|
||||
@@ -14,6 +15,11 @@ const Cartoon = {
|
||||
tubular_helices: OptionalField(bool, false, 'Simplify corkscrew helices to tubes.'),
|
||||
};
|
||||
|
||||
const Backbone = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
};
|
||||
|
||||
const BallAndStick = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
@@ -21,6 +27,13 @@ const BallAndStick = {
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Line = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
ignore_hydrogens: OptionalField(bool, false, 'Controls whether hydrogen atoms are drawn.'),
|
||||
};
|
||||
|
||||
const Spacefill = {
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
@@ -34,6 +47,8 @@ const Carbohydrate = {
|
||||
};
|
||||
|
||||
const Surface = {
|
||||
/** Type of surface representation. (Default is 'molecular') */
|
||||
surface_type: OptionalField(literal('molecular', 'gaussian'), 'molecular', `Type of surface representation. (Default is 'molecular')`),
|
||||
/** Scales the corresponding visuals */
|
||||
size_factor: OptionalField(float, 1, 'Scales the corresponding visuals.'),
|
||||
/** Controls whether hydrogen atoms are drawn. */
|
||||
@@ -45,7 +60,9 @@ export const MVSRepresentationParams = UnionParamsSchema(
|
||||
'Representation type',
|
||||
{
|
||||
cartoon: SimpleParamsSchema(Cartoon),
|
||||
backbone: SimpleParamsSchema(Backbone),
|
||||
ball_and_stick: SimpleParamsSchema(BallAndStick),
|
||||
line: SimpleParamsSchema(Line),
|
||||
spacefill: SimpleParamsSchema(Spacefill),
|
||||
carbohydrate: SimpleParamsSchema(Carbohydrate),
|
||||
surface: SimpleParamsSchema(Surface),
|
||||
@@ -63,10 +80,63 @@ const VolumeIsoSurface = {
|
||||
show_faces: OptionalField(bool, true, 'Show mesh faces. Defaults to true.'),
|
||||
};
|
||||
|
||||
const VolumeGridSlice = {
|
||||
/** Dimension of the grid slice, i.e. 'x', 'y', or 'z'. */
|
||||
dimension: RequiredField(literal('x', 'y', 'z'), 'Dimension of the grid slice, i.e. \'x\', \'y\', or \'z\'.'),
|
||||
/** Index of the grid slice in the specified dimension. 0-based index, i.e. 0 is the first slice. */
|
||||
absolute_index: OptionalField(nullable(int), null, 'Index of the grid slice in the specified dimension. 0-based index, i.e. 0 is the first slice.'),
|
||||
/** Relative index of the grid slice in the specified dimension. 0.0 is the first slice, 1.0 is the last slice. Overrides `absolute_index`. */
|
||||
relative_index: OptionalField(nullable(float), null, 'Relative index of the grid slice in the specified dimension. 0.0 is the first slice, 1.0 is the last slice. Overrides `absolute_index`.'),
|
||||
/** Relative isovalue. */
|
||||
relative_isovalue: OptionalField(nullable(float), null, 'Relative isovalue.'),
|
||||
/** Absolute isovalue. Overrides `relative_isovalue`. */
|
||||
absolute_isovalue: OptionalField(nullable(float), null, 'Absolute isovalue. Overrides `relative_isovalue`.'),
|
||||
};
|
||||
|
||||
export const MVSVolumeRepresentationParams = UnionParamsSchema(
|
||||
'type',
|
||||
'Representation type',
|
||||
{
|
||||
'isosurface': SimpleParamsSchema(VolumeIsoSurface),
|
||||
'grid_slice': SimpleParamsSchema(VolumeGridSlice),
|
||||
},
|
||||
);
|
||||
|
||||
const ClipParamsBase = {
|
||||
/** Transformation matrix to applied to each point before clipping. For example, can be used to clip volumes in the grid/fractional space. Default is null. */
|
||||
check_transform: OptionalField(nullable(Matrix), null, 'Transformation matrix to applied to each point before clipping. For example, can be used to clip volumes in the grid/fractional space. Default is null.'),
|
||||
/** Inverts the clipping region. Default is false. */
|
||||
invert: OptionalField(bool, false, 'Inverts the clipping region. Default is false'),
|
||||
/** Variant of the clip node, either "object" or "pixel". */
|
||||
variant: OptionalField(literal('object', 'pixel'), 'pixel', 'Variant of the clip node, either "object" or "pixel"'),
|
||||
};
|
||||
|
||||
export const MVSClipParams = UnionParamsSchema(
|
||||
'type',
|
||||
'Clip type',
|
||||
{
|
||||
plane: SimpleParamsSchema({
|
||||
...ClipParamsBase,
|
||||
/** Normal vector of the clipping plane. */
|
||||
normal: RequiredField(Vector3, 'Normal vector of the clipping plane.'),
|
||||
/** Point on the clipping plane. */
|
||||
point: RequiredField(Vector3, 'Point on the clipping plane.'),
|
||||
}),
|
||||
sphere: SimpleParamsSchema({
|
||||
...ClipParamsBase,
|
||||
/** Center of the clipping sphere. */
|
||||
center: RequiredField(Vector3, 'Center of the clipping sphere.'),
|
||||
/** Radius of the clipping sphere. */
|
||||
radius: OptionalField(float, 1, 'Radius of the clipping sphere.'),
|
||||
}),
|
||||
box: SimpleParamsSchema({
|
||||
...ClipParamsBase,
|
||||
/** Center of the clipping box. */
|
||||
center: RequiredField(Vector3, 'Center of the clipping box.'),
|
||||
/** Size of the clipping box. */
|
||||
size: OptionalField(Vector3, [1, 1, 1], 'Size of the clipping box.'),
|
||||
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
|
||||
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
}),
|
||||
},
|
||||
);
|
||||
@@ -5,12 +5,12 @@
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
|
||||
import { bool, dict, float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
|
||||
import { SimpleParamsSchema } from '../generic/params-schema';
|
||||
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
|
||||
import { MVSRepresentationParams, MVSVolumeRepresentationParams } from './mvs-tree-representations';
|
||||
import { MVSPrimitiveParams } from './mvs-tree-primitives';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
import { MVSClipParams, MVSRepresentationParams, MVSVolumeRepresentationParams } from './mvs-tree-representations';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, LabelAttachments, Matrix, Palette, ParseFormatT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
const _DataFromUriParams = {
|
||||
@@ -28,6 +28,8 @@ const _DataFromUriParams = {
|
||||
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
/** Optional remapping of annotation field names `{ standardName1: actualName1, ... }`. Use `{ "label_asym_id": "X" }` to load actual field "X" as "label_asym_id". Use `{ "label_asym_id": null }` to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name). */
|
||||
field_remapping: OptionalField(dict(str, nullable(str)), {}, 'Optional remapping of annotation field names `{ standardName1: actualName1, ... }`. Use `{ "label_asym_id": "X" }` to load actual field "X" as "label_asym_id". Use `{ "label_asym_id": null }` to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name).'),
|
||||
};
|
||||
|
||||
const _DataFromSourceParams = {
|
||||
@@ -41,11 +43,24 @@ const _DataFromSourceParams = {
|
||||
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
/** Optional remapping of annotation field names `{ standardName1: actualName1, ... }`. Use `{ "label_asym_id": "X" }` to load actual field "X" as "label_asym_id". Use `{ "label_asym_id": null }` to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name). */
|
||||
field_remapping: OptionalField(dict(str, nullable(str)), {}, 'Optional remapping of annotation field names `{ standardName1: actualName1, ... }`. Use `{ "label_asym_id": "X" }` to load actual field "X" as "label_asym_id". Use `{ "label_asym_id": null }` to ignore actual field "label_asym_id". Fields not mentioned here are mapped implicitely (i.e. actual name = standard name).'),
|
||||
};
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
|
||||
const TransformParams = SimpleParamsSchema({
|
||||
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
|
||||
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
|
||||
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
/** Point to rotate the object around. Can be either a 3D vector or dynamically computed object centroid. */
|
||||
rotation_center: OptionalField(nullable(union(Vector3, literal('centroid'))), null, 'Point to rotate the object around. Can be either a 3D vector or dynamically computed object centroid.'),
|
||||
/** Transform matrix (4x4 matrix flattened in column major format (j*4+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. Takes precedence over `rotation` and `translation`. */
|
||||
matrix: OptionalField(nullable(Matrix), null, 'Transform matrix (4x4 matrix flattened in column major format (j*4+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. Takes precedence over `rotation` and `translation`.'),
|
||||
});
|
||||
|
||||
/** Schema for `MVSTree` (MolViewSpec tree) */
|
||||
export const MVSTreeSchema = TreeSchema({
|
||||
rootKind: 'root',
|
||||
@@ -75,6 +90,12 @@ export const MVSTreeSchema = TreeSchema({
|
||||
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to retrieve molecular coordinates from a parsed data resource. */
|
||||
coordinates: {
|
||||
description: 'This node instructs to retrieve molecular coordinates from a parsed data resource.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({}),
|
||||
},
|
||||
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
|
||||
structure: {
|
||||
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
|
||||
@@ -96,18 +117,21 @@ export const MVSTreeSchema = TreeSchema({
|
||||
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Reference to a specific set of coordinates. */
|
||||
coordinates_ref: OptionalField(nullable(str), null, 'Reference to a specific set of coordinates.')
|
||||
}),
|
||||
},
|
||||
/** This node instructs to rotate and/or translate structure coordinates. */
|
||||
transform: {
|
||||
description: 'This node instructs to rotate and/or translate structure coordinates.',
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
|
||||
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
|
||||
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
}),
|
||||
description: 'This node instructs to rotate and/or translate coordinates OR provide a transformation matrix.',
|
||||
parent: ['structure', 'component', 'volume'],
|
||||
params: TransformParams,
|
||||
},
|
||||
/** This node allows instantiation using the provided transformation parameters. */
|
||||
instance: {
|
||||
description: 'This node allows instantiation using the provided transformation parameters.',
|
||||
parent: ['structure', 'component', 'volume'],
|
||||
params: TransformParams,
|
||||
},
|
||||
/** This node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component: {
|
||||
@@ -115,7 +139,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
parent: ['structure'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Defines what part of the parent structure should be included in this component. */
|
||||
selector: RequiredField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines what part of the parent structure should be included in this component.'),
|
||||
selector: RequiredField(union(ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)), 'Defines what part of the parent structure should be included in this component.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to create a component defined by an external annotation resource. */
|
||||
@@ -153,6 +177,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
description: 'This node instructs to create a volume from a parsed data resource. "Volume" refers to an internal representation of volumetric data without any visual representation.',
|
||||
parent: ['parse'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Channel identifier (only applies when the input data contain multiple channels). */
|
||||
channel_id: OptionalField(nullable(str), null, 'Channel identifier (only applies when the input data contain multiple channels).'),
|
||||
}),
|
||||
},
|
||||
@@ -170,7 +195,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
color: OptionalField(ColorT, DefaultColor, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
/** Defines to what part of the representation this color should be applied. */
|
||||
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
selector: OptionalField(union(ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)), 'all', 'Defines to what part of the representation this color should be applied.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
|
||||
@@ -181,6 +206,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromUriParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
/** Customize mapping of annotation values to colors. */
|
||||
palette: OptionalField(nullable(Palette), null, 'Customize mapping of annotation values to colors.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
@@ -191,8 +218,16 @@ export const MVSTreeSchema = TreeSchema({
|
||||
..._DataFromSourceParams,
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
|
||||
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
|
||||
/** Customize mapping of annotation values to colors. */
|
||||
palette: OptionalField(nullable(Palette), null, 'Customize mapping of annotation values to colors.'),
|
||||
}),
|
||||
},
|
||||
/** This node instructs to apply clipping to a visual representation. */
|
||||
clip: {
|
||||
description: 'This node instructs to apply clipping to a visual representation.',
|
||||
parent: ['representation', 'volume_representation', 'primitives', 'primitives_from_uri'],
|
||||
params: MVSClipParams,
|
||||
},
|
||||
/** This node instructs to apply opacity/transparency to a visual representation. */
|
||||
opacity: {
|
||||
description: 'This node instructs to apply opacity/transparency to a visual representation.',
|
||||
@@ -288,6 +323,8 @@ export const MVSTreeSchema = TreeSchema({
|
||||
position: RequiredField(Vector3, 'Coordinates of the camera.'),
|
||||
/** Vector which will be aligned with the screen Y axis. */
|
||||
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
|
||||
/** Near clipping plane distance from the position. */
|
||||
near: OptionalField(nullable(float), null, 'Near clipping plane distance from the position.'),
|
||||
}),
|
||||
},
|
||||
/** This node sets canvas properties. */
|
||||
@@ -296,7 +333,7 @@ export const MVSTreeSchema = TreeSchema({
|
||||
parent: ['root'],
|
||||
params: SimpleParamsSchema({
|
||||
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
background_color: OptionalField(ColorT, 'white', 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). Defaults to white.'),
|
||||
}),
|
||||
},
|
||||
primitives: {
|
||||
@@ -313,6 +350,16 @@ export const MVSTreeSchema = TreeSchema({
|
||||
opacity: OptionalField(float, 1, 'Opacity of primitive geometry in this group.'),
|
||||
/** Opacity of primitive labels in this group. */
|
||||
label_opacity: OptionalField(float, 1, 'Opacity of primitive labels in this group.'),
|
||||
/** Whether to show a tether line between the label and the target. Defaults to false. */
|
||||
label_show_tether: OptionalField(bool, false, 'Whether to show a tether line between the label and the target. Defaults to false.'),
|
||||
/** Length of the tether line between the label and the target. Defaults to 1 (Angstrom). */
|
||||
label_tether_length: OptionalField(float, 1, 'Length of the tether line between the label and the target. Defaults to 1 (Angstrom).'),
|
||||
/** How to attach the label to the target. Defaults to "middle-center". */
|
||||
label_attachment: OptionalField(LabelAttachments, 'middle-center', 'How to attach the label to the target. Defaults to "middle-center".'),
|
||||
/** Background color of the label. Defaults to none/transparent. */
|
||||
label_background_color: OptionalField(nullable(ColorT), null, 'Background color of the label. Defaults to none/transparent.'),
|
||||
/** Load snapshot with the provided key when interacting with this primitives group. */
|
||||
snapshot_key: OptionalField(nullable(str), null, 'Load snapshot with the provided key when interacting with this primitives group.'),
|
||||
/** Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices. */
|
||||
instances: OptionalField(nullable(list(Matrix)), null, 'Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices.'),
|
||||
}),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user