mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
287 Commits
support-sc
...
v5.0.0-dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33dc2015df | ||
|
|
fcf5ea420b | ||
|
|
8d97327f8d | ||
|
|
abc7ebba3e | ||
|
|
73d593907e | ||
|
|
0dc05e1138 | ||
|
|
dd11cacae4 | ||
|
|
b503259758 | ||
|
|
1e98741e16 | ||
|
|
f879519700 | ||
|
|
c6e175e5da | ||
|
|
add75bf9c9 | ||
|
|
57cbcd5fbf | ||
|
|
0a33936e06 | ||
|
|
7291025e09 | ||
|
|
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 | ||
|
|
08a929bb2f | ||
|
|
5a54b3ef66 | ||
|
|
a0c897547a | ||
|
|
89ce8394fd | ||
|
|
ea0331e95c | ||
|
|
9f220b55c2 | ||
|
|
acf248d58f | ||
|
|
c83b859766 | ||
|
|
33a2564893 | ||
|
|
d409c4f5ea | ||
|
|
ab61e31230 | ||
|
|
ae9c2dd9d8 | ||
|
|
c17edb4928 | ||
|
|
528377eb47 | ||
|
|
c9819369d0 | ||
|
|
cdbbbfa6dd | ||
|
|
a1e31c79e9 | ||
|
|
e027fe46c1 | ||
|
|
05c4006e9d | ||
|
|
191ea65c9d | ||
|
|
3c1ee16376 | ||
|
|
9ac34ee13b | ||
|
|
6778452d07 | ||
|
|
7e01af1e0d | ||
|
|
85469cbf28 | ||
|
|
299bdc72cd | ||
|
|
ae9f879139 | ||
|
|
b50d83d6ea | ||
|
|
2d99d8a1d0 | ||
|
|
ea00cca1c8 | ||
|
|
27c3b4e698 | ||
|
|
52942e7021 | ||
|
|
5904f694b5 | ||
|
|
92c0b82784 | ||
|
|
e1226fa384 | ||
|
|
ac2f7d1c38 | ||
|
|
ae1742f68e | ||
|
|
04e2da86fd | ||
|
|
510182ff60 | ||
|
|
4e1da19bdd | ||
|
|
a3eae15446 | ||
|
|
4334f4d1fa | ||
|
|
e33ed54121 | ||
|
|
ae8f037192 | ||
|
|
01271941dd | ||
|
|
7f8be5b8c6 | ||
|
|
2ab6e4b2e7 | ||
|
|
aa22840b12 | ||
|
|
c1e33fac94 | ||
|
|
a7336095ca | ||
|
|
4a88546181 | ||
|
|
edbc70cf6e | ||
|
|
c22ad2910c | ||
|
|
28a2b52e3c | ||
|
|
449d572ed5 | ||
|
|
470227af43 | ||
|
|
a0ccf46939 | ||
|
|
0ce8931fc5 | ||
|
|
3ddb29fc6f | ||
|
|
1a0c65df21 | ||
|
|
daad1923ea | ||
|
|
f34f879cf1 | ||
|
|
f47b76c8af | ||
|
|
6ee9eb8b60 | ||
|
|
915703a46d | ||
|
|
61c3c19ae3 | ||
|
|
6da9557531 | ||
|
|
29e6d69d21 | ||
|
|
6b2b87e6c5 | ||
|
|
5299d5c0c4 | ||
|
|
7bab95f4cc | ||
|
|
35e78ce638 | ||
|
|
3abbcb6949 | ||
|
|
c3fc893ad0 | ||
|
|
80415a2771 | ||
|
|
bfef69e2e4 | ||
|
|
a265a579be | ||
|
|
c2af1b0b22 | ||
|
|
b739876726 | ||
|
|
56851cc328 | ||
|
|
b719568555 | ||
|
|
f842f19912 | ||
|
|
d6b0dd910c | ||
|
|
395eb8e8d0 | ||
|
|
e4376c6737 | ||
|
|
6f23fcba8a | ||
|
|
7d19bfdb9b | ||
|
|
6e4065f779 | ||
|
|
c42a68b560 | ||
|
|
c86d913b83 | ||
|
|
0f6fa5fe15 | ||
|
|
f664cb02b1 | ||
|
|
00a53de6e2 | ||
|
|
b1ce5c158e | ||
|
|
26826b61f9 | ||
|
|
f44f954a76 | ||
|
|
78aae8a2b4 | ||
|
|
92267d3264 | ||
|
|
73ed45564e | ||
|
|
8bc2ebbeff | ||
|
|
5306d5d15a | ||
|
|
3c3fb461c8 | ||
|
|
62b3281282 | ||
|
|
e85fadf15b | ||
|
|
a8a84e1dbf | ||
|
|
153599ef89 | ||
|
|
b67eda7cb5 | ||
|
|
8b431c50be | ||
|
|
e195b048a1 | ||
|
|
ae5bb81b27 | ||
|
|
8c04c57bc5 | ||
|
|
ec46a444f1 | ||
|
|
559e0326b6 | ||
|
|
82b93bc2a8 | ||
|
|
62f940bc48 | ||
|
|
4e0be8e7b4 | ||
|
|
128502edf0 | ||
|
|
aad4d4a86c | ||
|
|
9bc7e27243 | ||
|
|
a5111356c1 | ||
|
|
9b11f7ffde | ||
|
|
93ce6d2807 | ||
|
|
5c9d5d3a3d | ||
|
|
f40307db39 | ||
|
|
4e6000fa6c | ||
|
|
26e5817bf2 | ||
|
|
8469be80d0 | ||
|
|
029edc95c8 | ||
|
|
dd9aaf055f | ||
|
|
fcfb2d940c | ||
|
|
f5b5109d0f | ||
|
|
ca99c800f1 | ||
|
|
dbd5570370 | ||
|
|
3f805c7a82 | ||
|
|
12c71dc5ba | ||
|
|
2fe3a926aa | ||
|
|
60c2096575 | ||
|
|
f4e9df5e4d | ||
|
|
c304b82772 | ||
|
|
9edd171350 | ||
|
|
f7d1bd7c04 | ||
|
|
7422c255ab | ||
|
|
5497215784 | ||
|
|
577bf1c77c | ||
|
|
c9bddccaf7 | ||
|
|
ac292f9267 | ||
|
|
f0b8d75b10 | ||
|
|
0dacbcb3bc | ||
|
|
0789241ea3 | ||
|
|
ddb0799dc4 | ||
|
|
cbfa341fa3 | ||
|
|
1c19bd90df | ||
|
|
300e5c8985 | ||
|
|
0861a78db6 | ||
|
|
a8e403ad85 | ||
|
|
4e350496b2 | ||
|
|
dd21ddcc80 | ||
|
|
a88121f779 | ||
|
|
1ab91d1979 | ||
|
|
267788388d | ||
|
|
43c0333be3 | ||
|
|
3b90a269b0 | ||
|
|
4aa5e1d7fc | ||
|
|
679db48938 | ||
|
|
2f96b42df7 | ||
|
|
dd6f3bd76e | ||
|
|
f1b7e478c7 | ||
|
|
416442aa27 | ||
|
|
a5f65b6e6f | ||
|
|
938ac0cc8f | ||
|
|
7dacf60478 | ||
|
|
9cdb8a3a92 | ||
|
|
242982e661 | ||
|
|
6da20a6989 | ||
|
|
f27b651230 | ||
|
|
7c818c0cc9 | ||
|
|
e7d7ba26b0 | ||
|
|
7e64121059 | ||
|
|
894bba1d3a | ||
|
|
d9db775fe8 | ||
|
|
a7fbc7b4c4 | ||
|
|
c0596298d6 | ||
|
|
8f32dde599 | ||
|
|
4d8f00900d |
@@ -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"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
build/
|
||||
deploy/
|
||||
lib/
|
||||
docs/site/
|
||||
|
||||
@@ -11,4 +12,8 @@ tsconfig.commonjs.tsbuildinfo
|
||||
*.sublime-workspace
|
||||
.idea
|
||||
|
||||
.DS_Store
|
||||
.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/*"],
|
||||
},
|
||||
}
|
||||
}
|
||||
238
CHANGELOG.md
238
CHANGELOG.md
@@ -4,18 +4,242 @@ All notable changes to this project will be documented in this file, following t
|
||||
Note that since we don't clearly distinguish between a public and private interfaces there will be changes in non-major versions that are potentially breaking. If we make breaking changes to less used interfaces we will highlight it in here.
|
||||
|
||||
## [Unreleased]
|
||||
- [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`
|
||||
- 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_reprepresentation_params`
|
||||
- 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), and fog
|
||||
- `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
|
||||
- Add `instance` node type
|
||||
- Support transforming and instancing of structures, components, and volumes
|
||||
- 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 from MVSX files
|
||||
- Indicate external links with ⤴
|
||||
- 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 "Download MVS State" link
|
||||
- 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.
|
||||
|
||||
## [v4.18.0] - 2025-06-08
|
||||
- MolViewSpec extension:
|
||||
- Support for label_comp_id and auth_comp_id in annotations
|
||||
- Geometric primitives - do not render if position refers to empty substructure
|
||||
- Primitive arrow - nicer default cap size (relative to tube_radius)
|
||||
- Primitive angle_measurement - added vector_radius param
|
||||
- Fix MVSX file assets being disposed in multi-snapshot states
|
||||
- Add `mol-utils/camera.ts` with `fovAdjustedPosition` and `fovNormalizedCameraPosition`
|
||||
- Show FOV normalized position in `CameraInfo` UI and use it in "Copy MVS State"
|
||||
- Support static resources in `AssetManager`
|
||||
- General:
|
||||
- Use `isolatedModules` tsconfig flag
|
||||
- Fix TurboPack build when using ES6 modules
|
||||
- Support `pickingAlphaThreshold` when `xrayShaded` is enabled
|
||||
- Support sampling from arbitrary planes for structure plane and volume slice representations
|
||||
- Refactor SCSS to not use `@import` (fixes deprecation warnings)
|
||||
|
||||
## [v4.17.0] - 2025-05-22
|
||||
- Remove `xhr2` dependency for NodeJS, use `fetch`
|
||||
- Add `mvs-stories` app included in the `molstar` NPM package
|
||||
- Use the app in the corresponding example
|
||||
- Interactions extension: remove `salt-bridge` interaction kind (since `ionic` is supported too)
|
||||
|
||||
## [v4.16.0] - 2025-05-20
|
||||
- Load potentially big text files as `StringLike` to bypass string size limit
|
||||
- MolViewSpec extension:
|
||||
- Load single-state MVS as if it were multi-state with one state
|
||||
- Merged `loadMVS` options `keepCamera` and `keepSnapshotCamera` -> `keepCamera`
|
||||
- Removed `loadMVS` option `replaceExisting` (is now default)
|
||||
- Added `loadMVS` option `appendSnapshots`
|
||||
- Fix camera not being interpolated in MP4 export due to updates in WebGL ContextLost handling
|
||||
|
||||
## [v4.15.0] - 2025-05-19
|
||||
- IHM improvements:
|
||||
- Disable volume streaming
|
||||
- Disable validation report visualization
|
||||
- Enable assembly symmetry for integrative models
|
||||
- Fix transparency rendering with occlusion in NodeJS
|
||||
- mmCIF Support
|
||||
- Add custom `molstar_bond_site` category that enables serializing explicit bonds by referencing `atom_site.id`
|
||||
- Add `includeCategoryNames`, `keepAtomSiteId`, `exportExplicitBonds`, `encoder` properties to `to_mmCIF` exporter
|
||||
- Add support for attachment points property (`M APO`) to the MOL V2000 parser
|
||||
- Add `json-cif` extension that should pave way towards structure editing capabilities in Mol\*
|
||||
- JSON-based encoding of the CIF data format
|
||||
- `JSONCifLigandGraph` that enables editing of small molecules via modifying `atom_site` and `molstar_bond_site` categories
|
||||
- Add `ligand-editor` example that showcases possible use-cases of the `json-cif` extension
|
||||
- Breaking (minor): Changed `atom_site.id` indexing to 1-based in `mol-model-formats/structure/mol.ts::getMolModels`.
|
||||
- WebGL ContextLost handling
|
||||
- Fix missing framebuffer & drawbuffer re-attachments
|
||||
- Fix missing cube texture re-initialization
|
||||
- Fix missing extensions reset
|
||||
- Fix timer clearing edge case
|
||||
- Add reset support for geometry generated on the GPU
|
||||
|
||||
## [v4.14.1] - 2025-05-09
|
||||
- Do not raise error when creating duplicate state transformers and print console warning instead
|
||||
|
||||
## [v4.14.0] - 2025-05-07
|
||||
- Fix `Viewer.loadTrajectory` when loading a topology file
|
||||
- Fix `StructConn.residueCantorPairs` to not include identity pairs
|
||||
- Add format selection option to image export UI (PNG, WebP, JPEG)
|
||||
- Add `StateBuilder.To.updateState`
|
||||
- MVS:
|
||||
- Support updating transform states
|
||||
- Add support for `is_hidden` custom state as an extension
|
||||
- Add `queryMVSRef` and `createMVSRefMap` utility functions
|
||||
- Adjust max resolution of surfaces for auto quality (#1501)
|
||||
- Fix switching representation type in Volume UI
|
||||
- VolumeServer: Avoid grid expansion when requiring unit cell (avoids including an extra layer of cells outside the unit cell query box)
|
||||
|
||||
## [v4.13.0] - 2025-04-14
|
||||
- Support `--host` option for build-dev.mjs script
|
||||
- Add `Viewer.loadFiles` to open supported files
|
||||
- Support installing the viewer as a Progressive Web App (PWA)
|
||||
- `ihm-restraints` example: show entity labels
|
||||
- Fix `element-point` visual not using child unit
|
||||
- Ignore `renderables` with empty draw count
|
||||
- Add experimental support for `esbuild` for development
|
||||
- Use `npm run dev` for faster development builds
|
||||
- Use `StructureElement.Bundle` instead of expressions to serialize measurement elements
|
||||
- Fixes measurements not being supported for coarse models
|
||||
- Implementation of `ColorScale.createDiscrete` (#1458)
|
||||
- Add `ColorScale.createDiscrete` to the `uncertainty` color theme
|
||||
- Fix color palette shown in the UI (for non-gradient palettes)
|
||||
- Fix colors description in the UI (when using custom thresholds)
|
||||
- Fix an edge case in the UI when the user deletes all colors from the color list
|
||||
- Add `interactions` extension and a corresponding example that utilizes it
|
||||
- Add element source index to default atomic granularity hover labels
|
||||
- Add `StructureElement.Schema` based on corresponding MolViewSpec implementation that allows data-driven selection of structural elements
|
||||
- Add `StructureElement.Loci/Bundle.fromExpression/Query/Schema` helper functions
|
||||
- Add `addLinkCylinderMesh` (from `createLinkCylinderMesh`)
|
||||
- Add `Unit.transientCache` and `Unit.getCopy`
|
||||
- Fix `ElementBondIterator` indices mapping logic for inter-unit bonds
|
||||
- Fix `pickPadding` and `pickScale` not updating `PickHelper`
|
||||
- MolViewSpec extension: support loading extensions when loading multistate files
|
||||
- Do not add bonds for pairs of residues that have a `struct_conn` entry
|
||||
- Improved `ma_qa_metric` support
|
||||
- Parse all local metrics
|
||||
- Ability to select alternate metrics in the pLDDT/qmean themes
|
||||
- Do not assume PAE plot is symmetric
|
||||
- Added `PluginConfig.Viewport.ShowScreenshotControls` to control visibility of screenshot controls
|
||||
- Fix MolViewSpec builder for volumes.
|
||||
- Generalize `mvs-kinase-story` example to `mvs-stories`
|
||||
- Add TATA-binding protein story
|
||||
- Improve the Kinase story
|
||||
- Fix alpha orbitals example
|
||||
|
||||
## [v4.12.0] - 2025-02-28
|
||||
|
||||
- Fix PDBj structure data URL
|
||||
- Improve logic when to cull in renderer
|
||||
- Add `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` symbol to the query language
|
||||
- MolViewSpec extension:
|
||||
- Add box, arrow, ellipse, ellipsoid, angle primitives
|
||||
- Add basic support for volumetric data (map, Volume Server)
|
||||
- Add support for `molstar_color_theme_name` custom extension
|
||||
- Better IH/M support:
|
||||
- Support `coarse` components
|
||||
- Support `spacefill` representation
|
||||
- Support `carbohydrate` representation
|
||||
- Support for `custom.molstar_use_default_coloring` property on Color node.
|
||||
- Use `atom.ihm.has-seq-id` and `atom.ihm.overlaps-seq-id-range` for matching `label_seq_id` locations to support querying coarse elements.
|
||||
- Add ihm-restraints example
|
||||
- Add `mvs-kinase-story` example
|
||||
- Remove static uses of `ColorTheme` and `SizeTheme` fields. Should resolvent "undefined" errors in certain builds
|
||||
- Add `transform` property to clip objects
|
||||
- Add support for trimming `image` geometry to a box
|
||||
- Improve/fix iso-level support of `slice` representation
|
||||
- Add support for rotating `slice` representation around an axis
|
||||
- Add default color support for palette based themes
|
||||
- Add `plane` structure representation
|
||||
- Can be colored with any structure theme
|
||||
- Can be colored with the `external-volume` theme
|
||||
- Can show atoms as a cutout
|
||||
- Supports principal axes and bounding box as a reference frame
|
||||
- Add `Camera` section to "Screenshot / State" controls
|
||||
- Add `CoarseIndex` for fast lookup of coarse elements
|
||||
|
||||
## [v4.11.0] - 2025-01-26
|
||||
|
||||
- Fix for tubular helices issue (Fixes #1422)
|
||||
- Volume UI improvements
|
||||
- Render all volume entries instead of selecting them one-by-one
|
||||
- Toggle visibility of all volumes
|
||||
- More accessible iso value control
|
||||
- Render all volume entries instead of selecting them one-by-one
|
||||
- Toggle visibility of all volumes
|
||||
- More accessible iso value control
|
||||
- Support wheel event on sliders
|
||||
- MolViewSpec extension:
|
||||
- Add validation for discriminated union params
|
||||
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
|
||||
- Add validation for discriminated union params
|
||||
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
|
||||
- UI configuration options
|
||||
- Support removal of independent selection controls in the viewport
|
||||
- Support custom selection controls
|
||||
- Support for custom granularity dropdown options
|
||||
- Support for custom Sequence Viewer mode options
|
||||
- Add `external-structure` theme that colors any geometry by structure properties
|
||||
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
|
||||
- Minor documentation updates
|
||||
- Add support for position-location to `volume-value` color theme
|
||||
- Add support for color themes to `slice` representation
|
||||
- Improve/fix palette support in volume color themes
|
||||
- Fix `Plane3D.projectPoint`
|
||||
- Fix marking related `image` rendering issues
|
||||
- Handle pixels without a group
|
||||
- Take fog into account
|
||||
- MolViewSpec extension: Initial support for customizable representation parameters
|
||||
- Quick Styles section reorganized
|
||||
- UI color improvements (scrollbar contrast, toggle button hover color)
|
||||
- Add `overrideWater` param for entity-id color theme
|
||||
- Renames PDB-Dev to PDB-IHM and adjusts data source
|
||||
- Fix vertex based themes for spheres shader
|
||||
- Add volume dot representation
|
||||
- Add volume-value size theme
|
||||
- Sequence panel: Mark focused loci (bold+underline)
|
||||
- Change modifier key behavior in Normal Mode (default = select only, Ctrl/Cmd = add to selection, Shift = extend last selected range)
|
||||
- Handle Firefox's limit on vertex ids per draw (#1116)
|
||||
- Fix behavior of `Vec3.makeRotation(out, a, b)` when `a ≈ -b`
|
||||
|
||||
## [v4.10.0] - 2024-12-15
|
||||
|
||||
@@ -83,7 +307,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Fix `findPredecessorIndex` bug when repeating values
|
||||
- MolViewSpec: Support for transparency and custom properties
|
||||
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
|
||||
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-IHM/PDB-Dev loader
|
||||
- Fix Sequence View in Safari 18
|
||||
- Improve performance of `IndexPairBonds` assignment when operator keys are available
|
||||
- ModelArchive QualityAssessment extension:
|
||||
@@ -136,7 +360,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Improve entity-id coloring for structures with multiple models from the same source (#1221)
|
||||
- Wrap screenshot & image generation in a `Task`
|
||||
- AlphaFold DB: Add BinaryCIF support when fetching data
|
||||
- PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
|
||||
- PDB-IHM/PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
|
||||
- Fix polymer-gap visual coloring with cartoon theme
|
||||
- Add formal-charge color theme (#328)
|
||||
- Add more coloring options to cartoon theme
|
||||
|
||||
21
README.md
21
README.md
@@ -84,7 +84,16 @@ Wipes the `build` and `lib` directories and `.tsbuildinfo` files.
|
||||
|
||||
Runs the cleanup script prior to building the project, forcing a full rebuild of the project.
|
||||
|
||||
Use these commands to resolve occassional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
|
||||
Use these commands to resolve occasional build failures which may arise after some dependency updates. Once done, `npm run build` should work again. Note that full rebuilds take more time to complete.
|
||||
|
||||
### Develop with `esbuild`
|
||||
|
||||
Experimental support for faster builds with `esbuild`
|
||||
- `npm run dev:all` - watch mode for all apps and examples
|
||||
- `npm run dev:viewer` - watch mode for viewer
|
||||
- `npm run dev:apps` - watch mode for all apps
|
||||
- `npm run dev:examples` - watch mode for all examples
|
||||
- `npm run dev -- -a <app name 1> <app name 2> -e <example name 1> ...` - watch mode for specified apps/examples. `-a`/`-e` with without any names will build everything.
|
||||
|
||||
### Build for production:
|
||||
NODE_ENV=production npm run build
|
||||
@@ -103,7 +112,6 @@ From the root of the project:
|
||||
|
||||
and navigate to `build/viewer`
|
||||
|
||||
|
||||
### Code generation
|
||||
**CIF schemas**
|
||||
|
||||
@@ -182,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.
|
||||
|
||||
15
data/pwa/README.md
Normal file
15
data/pwa/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
The files in this directory are used for deploying the Mol* Viewer as a PWA (Progressive Web App) at https://molstar.org/viewer/. They may serve as an example for creating your own PWA but wont work as-is. See `/script/deploy.js` for where these files are copied and how they are transformed during deployment.
|
||||
|
||||
|
||||
## PWA features
|
||||
|
||||
- The Service Worker will cache static resources so the Viewer can be used without internet access. This works without installing, i.e., also in Firefox.
|
||||
- Once installed, file types listed in the Manifest can be opened from, e.g., the Windows File Explorer.
|
||||
|
||||
|
||||
## Notes for development
|
||||
|
||||
In Chrome you can see a list of installed PWAs at chrome://apps/. A right-click opens a menu with an option uninstall.
|
||||
|
||||
The Chrome Dev Tools have a section 'Application' to inspect and manage PWA aspects like the Manifest and Service Workers.
|
||||
BIN
data/pwa/logo-144.png
Normal file
BIN
data/pwa/logo-144.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
39
data/pwa/manifest.webmanifest
Normal file
39
data/pwa/manifest.webmanifest
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"id": "https://molstar.org/viewer/",
|
||||
"name": "Mol* Viewer",
|
||||
"short_name": "Mol*",
|
||||
"description": "Mol* Viewer: a modern web app for 3D visualization and analysis of large biomolecular structures.",
|
||||
"start_url": "./index.html",
|
||||
"theme_color": "#eeece7",
|
||||
"background_color": "#eeece7",
|
||||
"display": "standalone",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "48x48"
|
||||
},
|
||||
{
|
||||
"src": "logo-144.png",
|
||||
"sizes": "144x144"
|
||||
}
|
||||
],
|
||||
"file_handlers": [
|
||||
{
|
||||
"action": "./index.html",
|
||||
"accept": {
|
||||
"application/vnd.molstar": [".molx", ".molj"],
|
||||
"text/plain": [
|
||||
".mol", ".mol2", ".sdf", ".sd", ".pdb", ".ent", ".pdbqt", ".cif", ".mcif", ".mmcif", ".xyz", ".gro", ".lammpstrj",
|
||||
".cub", ".cube", ".dx"
|
||||
],
|
||||
"application/octet-stream": [
|
||||
".bcif",
|
||||
".dxbin", ".ccp4", ".mrc", ".map", ".dsn6", ".brix"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"launch_handler": {
|
||||
"client_mode": ["auto"]
|
||||
}
|
||||
}
|
||||
44
data/pwa/pwa.js
Normal file
44
data/pwa/pwa.js
Normal file
@@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Andy Turner <agdturner@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
window.addEventListener('molstarViewerCreated', e => {
|
||||
const viewer = e.detail.viewer;
|
||||
|
||||
// Handle incoming files
|
||||
if ('launchQueue' in window) {
|
||||
launchQueue.setConsumer((launchParams) => {
|
||||
if (!launchParams.files.length) return;
|
||||
|
||||
const files = [];
|
||||
for (const fileHandle of launchParams.files) {
|
||||
files.push(fileHandle.getFile());
|
||||
}
|
||||
|
||||
Promise.all(files).then((files) => {
|
||||
viewer.loadFiles(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Register Progressive Web App service worker.
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', function () {
|
||||
navigator.serviceWorker.register('./sw.js')
|
||||
.then(function (registration) {
|
||||
// Registration was successful
|
||||
if (molstar.isDebugMode) {
|
||||
console.log('ServiceWorker registration successful with scope: ', registration.scope);
|
||||
}
|
||||
}, function (err) {
|
||||
// registration failed :(
|
||||
if (molstar.isDebugMode) {
|
||||
console.error('ServiceWorker registration failed: ', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
60
data/pwa/sw.js
Normal file
60
data/pwa/sw.js
Normal file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Andy Turner <agdturner@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
/** version from package.json, to be filled in during deployment */
|
||||
const VERSION = '__MOLSTAR_VERSION__';
|
||||
|
||||
const CACHE_NAME = `molstar-viewer-${VERSION}`;
|
||||
|
||||
// The static resources that the app needs to function.
|
||||
const APP_STATIC_RESOURCES = [
|
||||
'favicon.ico',
|
||||
'index.html',
|
||||
'molstar.css',
|
||||
'molstar.js',
|
||||
'manifest.webmanifest',
|
||||
'logo-144.png',
|
||||
'pwa.js'
|
||||
];
|
||||
|
||||
async function cacheStaticResources() {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
await cache.addAll(APP_STATIC_RESOURCES);
|
||||
await self.skipWaiting(); // Ensures the new service worker takes control immediately.
|
||||
}
|
||||
|
||||
async function deleteOldCaches() {
|
||||
const keys = await caches.keys();
|
||||
await Promise.all(
|
||||
keys.map((key) => {
|
||||
if (key !== CACHE_NAME) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
}),
|
||||
);
|
||||
await self.clients.claim(); // Ensures the new service worker takes control immediately.
|
||||
}
|
||||
|
||||
async function respondWithCacheFirst(request) {
|
||||
// Try to match the request with the cache
|
||||
const cachedResponse = await caches.match(request);
|
||||
return cachedResponse || fetch(request);
|
||||
}
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
// console.log(`Service Worker version ${VERSION} installed.`);
|
||||
event.waitUntil(cacheStaticResources());
|
||||
});
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
// console.log(`Service Worker version ${VERSION} activated.`);
|
||||
event.waitUntil(deleteOldCaches());
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', (event) => {
|
||||
event.respondWith(respondWithCacheFirst(event.request));
|
||||
});
|
||||
@@ -24,7 +24,7 @@ npm install
|
||||
Afterwards, build the project source:
|
||||
|
||||
```
|
||||
npm run build-tsc
|
||||
npm run build:lib
|
||||
```
|
||||
|
||||
and run the server by
|
||||
|
||||
4
docs/docs/extensions/interactions.md
Normal file
4
docs/docs/extensions/interactions.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Interactions extension
|
||||
|
||||
The Interactions extension enables computing or providing custom interactions between multiple selections/structures.
|
||||
For usage, see the [example source code](https://github.com/molstar/molstar/tree/master/src/examples/interactions).
|
||||
@@ -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, {
|
||||
|
||||
@@ -25,7 +25,7 @@ import { PluginContext } from 'molstar/lib/mol-plugin/context';
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
npm run build
|
||||
```
|
||||
|
||||
--------------------
|
||||
|
||||
@@ -141,7 +141,7 @@ export async function loadStructure(plugin: PluginUIContext, url: string, option
|
||||
```
|
||||
- Create `src/style.scss`:
|
||||
```scss
|
||||
@import '../node_modules/molstar/lib/mol-plugin-ui/skin/light.scss';
|
||||
@use '../node_modules/molstar/lib/mol-plugin-ui/skin/light.scss';
|
||||
```
|
||||
- Create `build/ui.html`:
|
||||
```html
|
||||
|
||||
@@ -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.initViewer(canvas, parent))) {
|
||||
console.error('Failed to init Mol*');
|
||||
return;
|
||||
}
|
||||
|
||||
102
docs/docs/plugin/managers/markdown-extensions.md
Normal file
102
docs/docs/plugin/managers/markdown-extensions.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# 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
|
||||
- `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%3Dresn%20HEM%26lang%3Dpymol%26action%3Dhighlight%2Cfocus)` highlights or focuses the HEM residue (the command must be URL encoded because it contains spaces and possibly other special characters)
|
||||
|
||||
## 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
|
||||
)
|
||||
```
|
||||
@@ -70,7 +70,6 @@ This relies on the concept of `Expression` which is basically a intermediate rep
|
||||
### Select residues 10-15 of chains A and F in a structure using a `SelectionQuery` object:
|
||||
|
||||
```typescript
|
||||
|
||||
import { MolScriptBuilder as MS, MolScriptBuilder } from 'molstar/lib/mol-script/language/builder';
|
||||
import { Expression } from 'molstar/lib/mol-script/language/expression';
|
||||
import { StructureSelectionQuery } from 'molstar/lib/mol-plugin-state/helpers/structure-selection-query'
|
||||
@@ -82,8 +81,8 @@ export function select_multiple() {
|
||||
const groups: Expression[] = [];
|
||||
for (var chain of args) {
|
||||
groups.push(MS.struct.generator.atomGroups({
|
||||
"chain-test": MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
|
||||
"residue-test": MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
|
||||
'chain-test': MS.core.rel.eq([MolScriptBuilder.struct.atomProperty.macromolecular.auth_asym_id(), chain[0]]),
|
||||
'residue-test': MS.core.rel.inRange([MolScriptBuilder.struct.atomProperty.macromolecular.label_seq_id(), chain[1], chain[2]])
|
||||
}));
|
||||
}
|
||||
var sq = StructureSelectionQuery('residue_range_10_15_in_A_and_F', MS.struct.combinator.merge(groups))
|
||||
@@ -98,13 +97,52 @@ Inspect these examples to get a better feeling for this syntax: `https://github.
|
||||
|
||||
Furthermore, a query made this way can be converted to a `Loci` object which is important in many parts of the libary:
|
||||
```typescript
|
||||
|
||||
// Select residue 124 of chain A and convert to Loci
|
||||
const Q = MolScriptBuilder;
|
||||
var sel = Script.getStructureSelection(Q => Q.struct.generator.atomGroups({
|
||||
'chain-test' : Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
|
||||
"residue-test": Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
|
||||
}), objdata)
|
||||
'chain-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.auth_asym_id(), A]),
|
||||
'residue-test': Q.core.rel.eq([Q.struct.atomProperty.macromolecular.label_seq_id(), 124]),
|
||||
}), objdata)
|
||||
|
||||
let loci = StructureSelection.toLociWithSourceUnits(sel);
|
||||
```
|
||||
|
||||
## Query Functions
|
||||
|
||||
Instead of building expressions, query functions can be created directly, e.g.:
|
||||
|
||||
```ts
|
||||
import { atoms } from 'mol-model/structure/query/queries/generators';
|
||||
|
||||
const query = atoms({
|
||||
residueTest: ctx => {
|
||||
const seqId = StructureProperties.residue.label_seq_id(ctx.element);
|
||||
return seqId > 10 && seqId < 25;
|
||||
},
|
||||
});
|
||||
|
||||
const selection = query(new QueryContext(structure));
|
||||
// ...
|
||||
```
|
||||
|
||||
## Selection Schema
|
||||
|
||||
For simple selections, the `StructureElement.Schema` can be used to reference elements within a protein structure using mmCIF `atom_site` field names, e.g.:
|
||||
|
||||
```ts
|
||||
const ala121: StructureElement.Schema = { label_asym_id: 'A', label_seq_id: 121 };
|
||||
const residues: StructureElement.Schema = {
|
||||
items: {
|
||||
auth_asym_id: ['A', 'B'],
|
||||
auth_seq_id: [10, 11],
|
||||
}
|
||||
};
|
||||
|
||||
const loci = StructureElement.Loci.fromSchema(structure, residues);
|
||||
```
|
||||
|
||||
Usually, a code editor such as VS Code will auto-suggest all the available field names.
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Given an `Expression`, `QueryFn`, or `StructureElement.Schema` it is possible to use `fromExpression/Query/Schema` functions on `StructureElement.Loci` and `StructureElement.Bundle`.
|
||||
@@ -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'
|
||||
@@ -56,7 +57,8 @@ nav:
|
||||
- MolViewSpec: 'extensions/mvs/index.md'
|
||||
- wwPDB StructConn: 'extensions/struct-conn.md'
|
||||
- Tunnels: 'extensions/tunnels.md'
|
||||
- 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
|
||||
|
||||
110
eslint.config.mjs
Normal file
110
eslint.config.mjs
Normal file
@@ -0,0 +1,110 @@
|
||||
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",
|
||||
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],
|
||||
},
|
||||
}]);
|
||||
456
examples/docking/ligands_1.sdf
Normal file
456
examples/docking/ligands_1.sdf
Normal file
@@ -0,0 +1,456 @@
|
||||
forcefield_3904
|
||||
RDKit 3D
|
||||
|
||||
49 52 0 0 0 0 0 0 0 0999 V2000
|
||||
7.1950 23.7840 6.1780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.7810 23.6440 6.1320 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.0380 24.7500 6.4550 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.8270 25.8570 5.6380 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.0460 26.9150 6.0970 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4760 26.8670 7.3710 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6720 25.7630 8.2160 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.0720 25.7780 9.4800 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.8090 25.4470 9.9650 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8080 26.1730 9.4160 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.4770 25.9480 9.8040 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5110 26.6810 9.2540 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.0950 27.9270 8.6960 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1890 28.9860 8.9110 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6300 30.4110 8.7600 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7130 31.4650 8.9780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4040 31.2910 10.3270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.9520 29.8780 10.4990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.8690 28.8240 10.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7400 24.9710 10.7610 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.8850 24.4940 11.3660 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4690 23.5570 12.1930 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1090 23.4080 12.1480 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.3700 24.3040 11.2380 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.6480 24.4860 10.8910 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.4600 24.7130 7.7330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.5263 23.8168 7.2264 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.6649 22.9280 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.4879 24.7155 5.6716 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.2751 25.8961 4.6342 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8776 27.7913 5.4538 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.8616 27.7104 7.7192 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6420 26.0930 10.2520 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1673 28.2555 9.1874 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.9189 27.8009 7.6175 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.9466 28.8274 8.1294 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8310 30.5615 9.5009 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2424 30.5222 7.7366 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2524 32.4632 8.9392 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4689 31.3496 8.1873 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6781 31.4924 11.1285 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.2475 31.9954 10.3749 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.3529 29.7723 11.5179 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7404 29.7213 9.7481 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1096 28.9239 11.0705 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.3427 27.8318 10.3148 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1334 22.9663 12.8408 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.4510 22.7600 12.6820 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.6312 23.8345 8.3724 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 1 0
|
||||
3 4 2 0
|
||||
4 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 1 0
|
||||
8 9 1 0
|
||||
9 10 2 0
|
||||
10 11 1 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
11 20 2 0
|
||||
20 21 1 0
|
||||
21 22 2 0
|
||||
22 23 1 0
|
||||
23 24 1 0
|
||||
24 25 2 0
|
||||
7 26 2 0
|
||||
26 3 1 0
|
||||
25 9 1 0
|
||||
19 14 1 0
|
||||
24 20 1 0
|
||||
1 27 1 0
|
||||
1 28 1 0
|
||||
1 29 1 0
|
||||
4 30 1 0
|
||||
5 31 1 0
|
||||
6 32 1 0
|
||||
8 33 1 0
|
||||
13 34 1 0
|
||||
13 35 1 0
|
||||
14 36 1 0
|
||||
15 37 1 0
|
||||
15 38 1 0
|
||||
16 39 1 0
|
||||
16 40 1 0
|
||||
17 41 1 0
|
||||
17 42 1 0
|
||||
18 43 1 0
|
||||
18 44 1 0
|
||||
19 45 1 0
|
||||
19 46 1 0
|
||||
22 47 1 0
|
||||
23 48 1 0
|
||||
26 49 1 0
|
||||
M END
|
||||
> <ligandCode> (1)
|
||||
forcefield_3904
|
||||
|
||||
> <ligandName> (1)
|
||||
klr_22
|
||||
|
||||
$$$$
|
||||
forcefield_3905
|
||||
RDKit 3D
|
||||
|
||||
49 52 0 0 0 0 0 0 0 0999 V2000
|
||||
6.5460 25.1350 3.8360 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.2550 25.5560 3.9960 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.5630 25.8360 3.0240 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7190 25.6170 5.3820 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.0120 26.7570 5.7730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4880 26.8570 7.0690 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.6630 25.8340 8.0050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.1640 25.8720 9.3110 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.9510 25.5250 9.9030 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8900 26.1930 9.3960 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.3560 25.9480 9.8850 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4490 26.6220 9.3800 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.1050 27.7730 8.6070 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7010 29.0180 9.2810 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5340 30.2520 8.3770 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5490 31.3650 8.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.0430 31.3400 10.0950 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7320 30.0170 10.4330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.1730 28.8170 9.6630 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5160 25.0110 10.9050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5980 24.5310 11.6140 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.0930 23.6490 12.4490 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.2630 23.5390 12.3140 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.6490 24.4020 11.3310 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.8910 24.6070 10.8850 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3890 24.7010 7.5990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.9170 24.5890 6.3060 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
7.0340 24.6220 4.5560 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.8360 24.9760 2.8790 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8658 27.5811 5.0592 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.9275 27.7591 7.3554 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7800 26.2290 10.0270 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.0105 27.8693 8.5543 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5056 27.6720 7.5875 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.1446 29.1858 10.2149 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5231 30.6584 8.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7027 29.9170 7.3429 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0722 32.3370 8.4626 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4156 31.2068 7.9983 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1848 31.4776 10.7693 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7762 32.1504 10.2201 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.6091 29.8266 11.5094 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7879 30.1213 10.1428 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.2575 27.9194 10.2932 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7539 28.7064 8.7355 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6897 23.0698 13.1690 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8800 22.9330 12.8360 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.5467 23.8809 8.3150 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.4866 23.6927 6.0193 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 1 0
|
||||
4 5 2 0
|
||||
5 6 1 0
|
||||
6 7 2 0
|
||||
7 8 1 0
|
||||
8 9 1 0
|
||||
9 10 2 0
|
||||
10 11 1 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
11 20 2 0
|
||||
20 21 1 0
|
||||
21 22 2 0
|
||||
22 23 1 0
|
||||
23 24 1 0
|
||||
24 25 2 0
|
||||
7 26 1 0
|
||||
26 27 2 0
|
||||
27 4 1 0
|
||||
25 9 1 0
|
||||
19 14 1 0
|
||||
24 20 1 0
|
||||
1 28 1 0
|
||||
1 29 1 0
|
||||
5 30 1 0
|
||||
6 31 1 0
|
||||
8 32 1 0
|
||||
13 33 1 0
|
||||
13 34 1 0
|
||||
14 35 1 0
|
||||
15 36 1 0
|
||||
15 37 1 0
|
||||
16 38 1 0
|
||||
16 39 1 0
|
||||
17 40 1 0
|
||||
17 41 1 0
|
||||
18 42 1 0
|
||||
18 43 1 0
|
||||
19 44 1 0
|
||||
19 45 1 0
|
||||
22 46 1 0
|
||||
23 47 1 0
|
||||
26 48 1 0
|
||||
27 49 1 0
|
||||
M END
|
||||
> <ligandCode> (2)
|
||||
forcefield_3905
|
||||
|
||||
> <ligandName> (2)
|
||||
1oiy-1
|
||||
|
||||
$$$$
|
||||
forcefield_14264
|
||||
RDKit 3D
|
||||
|
||||
50 53 0 0 0 0 0 0 0 0999 V2000
|
||||
4.9220 23.4040 3.0090 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.8970 24.7200 3.4040 S 0 0 0 0 0 6 0 0 0 0 0 0
|
||||
7.2120 24.2200 3.7260 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.6670 25.6980 2.3670 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.1310 25.2890 4.8970 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3720 26.4580 4.8910 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7730 26.8990 6.0760 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.9290 26.1970 7.2780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.3430 26.5790 8.4890 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.1400 26.2820 9.1290 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.0830 26.9670 8.6370 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1520 26.7740 9.1730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2430 27.4630 8.6800 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.9280 28.7750 8.2140 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2060 29.6320 8.2420 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.2120 29.0810 9.2730 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.1810 30.1570 9.7450 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.4520 31.3060 10.4440 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0250 31.5210 9.9360 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.8770 31.1190 8.4740 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.3060 25.8730 10.2240 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.3770 25.4460 10.9810 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8720 24.5730 11.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.4750 24.4190 11.6520 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.8530 25.2430 10.6320 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.0880 25.3990 10.1410 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7090 25.0260 7.2550 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.3130 24.5730 6.0780 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.1960 23.3080 3.7210 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.4460 22.5390 2.8810 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.2447 27.0304 3.9603 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.1666 27.8167 6.0634 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.8380 27.2640 9.0420 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1661 29.2261 8.8668 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.5358 28.7227 7.1876 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6885 29.5690 7.2555 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.7847 28.2635 8.8106 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.6467 28.7218 10.1456 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7231 30.5555 8.8747 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.8753 29.7014 10.4664 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.0232 32.2317 10.2804 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.3803 31.0434 11.5098 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7667 32.5851 10.0403 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.3511 30.8895 10.5336 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5603 31.7316 7.8675 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8284 31.2816 8.1841 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4619 24.0333 12.5825 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.0910 23.8110 12.1720 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.8468 24.4538 8.1843 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.9261 23.6598 6.0846 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 2 0
|
||||
2 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 2 0
|
||||
8 9 1 0
|
||||
9 10 1 0
|
||||
10 11 2 0
|
||||
11 12 1 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
19 20 1 0
|
||||
12 21 2 0
|
||||
21 22 1 0
|
||||
22 23 2 0
|
||||
23 24 1 0
|
||||
24 25 1 0
|
||||
25 26 2 0
|
||||
8 27 1 0
|
||||
27 28 2 0
|
||||
28 5 1 0
|
||||
26 10 1 0
|
||||
20 15 1 0
|
||||
25 21 1 0
|
||||
1 29 1 0
|
||||
1 30 1 0
|
||||
6 31 1 0
|
||||
7 32 1 0
|
||||
9 33 1 0
|
||||
14 34 1 0
|
||||
14 35 1 0
|
||||
15 36 1 0
|
||||
16 37 1 0
|
||||
16 38 1 0
|
||||
17 39 1 0
|
||||
17 40 1 0
|
||||
18 41 1 0
|
||||
18 42 1 0
|
||||
19 43 1 0
|
||||
19 44 1 0
|
||||
20 45 1 0
|
||||
20 46 1 0
|
||||
23 47 1 0
|
||||
24 48 1 0
|
||||
27 49 1 0
|
||||
28 50 1 0
|
||||
M END
|
||||
> <ligandCode> (3)
|
||||
forcefield_14264
|
||||
|
||||
> <ligandName> (3)
|
||||
1h1s
|
||||
|
||||
$$$$
|
||||
forcefield_14265
|
||||
RDKit 3D
|
||||
|
||||
50 53 0 0 0 0 0 0 0 0999 V2000
|
||||
5.9560 25.0880 4.1850 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.6880 24.2300 5.6100 S 0 0 0 0 0 6 0 0 0 0 0 0
|
||||
4.9010 23.0800 5.2270 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.9470 24.1160 6.3060 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.6500 25.3510 6.5050 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.3190 26.5790 5.9340 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.4970 27.4490 6.6410 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.0220 27.0950 7.9100 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.3680 25.8770 8.5100 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.9060 25.4620 9.7630 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.6520 25.1280 10.2760 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.6590 25.9320 9.8350 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.6150 25.7270 10.2650 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.6410 26.5420 9.8330 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2650 27.5510 8.8940 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.4150 28.9670 9.4830 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0650 29.8960 8.4420 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7130 31.1080 9.0990 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8410 30.6980 10.0490 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.6490 29.3080 10.6570 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1750 28.9600 10.8270 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.8720 24.6910 11.1610 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0060 24.2140 11.7870 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.5910 23.2030 12.5210 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2430 23.0050 12.3980 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.2320 23.9470 11.5330 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.5000 24.1080 11.1380 N 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.1890 25.0030 7.7800 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
6.4350 24.4790 3.5180 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
5.1080 25.4920 3.7910 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.7020 26.8549 4.9404 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.2187 28.4186 6.2023 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.3612 27.7900 8.4488 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.5640 25.4960 10.5280 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2150 27.3950 8.6054 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.9312 27.4715 8.0223 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.4134 29.3591 9.7135 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.2920 30.2413 7.7397 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.8482 29.3296 7.9168 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.9484 31.6568 9.6685 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.1423 31.7400 8.3076 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8921 31.4320 10.8667 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.7682 30.6682 9.4579 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.1367 29.2806 11.6426 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-4.0960 28.5721 9.9724 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-1.7122 29.6974 11.4994 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1140 27.9439 11.2440 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.2486 22.5915 13.1563 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.3120 22.2980 12.8550 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.4733 24.0335 8.2150 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0
|
||||
2 3 2 0
|
||||
2 4 2 0
|
||||
2 5 1 0
|
||||
5 6 2 0
|
||||
6 7 1 0
|
||||
7 8 2 0
|
||||
8 9 1 0
|
||||
9 10 1 0
|
||||
10 11 1 0
|
||||
11 12 2 0
|
||||
12 13 1 0
|
||||
13 14 1 0
|
||||
14 15 1 0
|
||||
15 16 1 0
|
||||
16 17 1 0
|
||||
17 18 1 0
|
||||
18 19 1 0
|
||||
19 20 1 0
|
||||
20 21 1 0
|
||||
13 22 2 0
|
||||
22 23 1 0
|
||||
23 24 2 0
|
||||
24 25 1 0
|
||||
25 26 1 0
|
||||
26 27 2 0
|
||||
9 28 2 0
|
||||
28 5 1 0
|
||||
27 11 1 0
|
||||
21 16 1 0
|
||||
26 22 1 0
|
||||
1 29 1 0
|
||||
1 30 1 0
|
||||
6 31 1 0
|
||||
7 32 1 0
|
||||
8 33 1 0
|
||||
10 34 1 0
|
||||
15 35 1 0
|
||||
15 36 1 0
|
||||
16 37 1 0
|
||||
17 38 1 0
|
||||
17 39 1 0
|
||||
18 40 1 0
|
||||
18 41 1 0
|
||||
19 42 1 0
|
||||
19 43 1 0
|
||||
20 44 1 0
|
||||
20 45 1 0
|
||||
21 46 1 0
|
||||
21 47 1 0
|
||||
24 48 1 0
|
||||
25 49 1 0
|
||||
28 50 1 0
|
||||
M END
|
||||
> <ligandCode> (4)
|
||||
forcefield_14265
|
||||
|
||||
> <ligandName> (4)
|
||||
1oiu
|
||||
|
||||
$$$$
|
||||
9013
examples/docking/receptor_1.pdb
Normal file
9013
examples/docking/receptor_1.pdb
Normal file
File diff suppressed because it is too large
Load Diff
4052
examples/mvs/kinase-story.mvsj
Normal file
4052
examples/mvs/kinase-story.mvsj
Normal file
File diff suppressed because it is too large
Load Diff
32658
package-lock.json
generated
32658
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
105
package.json
105
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.10.0",
|
||||
"version": "5.0.0-dev.4",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -10,29 +10,30 @@
|
||||
"bugs": {
|
||||
"url": "https://github.com/molstar/molstar/issues"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.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 ./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",
|
||||
@@ -43,7 +44,8 @@
|
||||
},
|
||||
"files": [
|
||||
"lib/",
|
||||
"build/viewer/"
|
||||
"build/viewer/",
|
||||
"build/mvs-stories/"
|
||||
],
|
||||
"bin": {
|
||||
"cif2bcif": "lib/commonjs/cli/cif2bcif/index.js",
|
||||
@@ -114,68 +116,65 @@
|
||||
"Eric E <etongfu@outlook.com>",
|
||||
"Xavier Martinez <xavier.martinez.xm@gmail.com>",
|
||||
"Alex Chan <smalldirkalex@gmail.com>",
|
||||
"Simeon Borko <simeon.borko@gmail.com>"
|
||||
"Simeon Borko <simeon.borko@gmail.com>",
|
||||
"Ventura Rivera <venturaxrivera@gmail.com>",
|
||||
"Andy Turner <agdturner@gmail.com>",
|
||||
"Lukáš Polák <admin@lukaspolak.cz>",
|
||||
"Chetan Mishra <chetan.s115@gmail.com>",
|
||||
"Zach Charlop-Powers <zach.charlop.powers@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.16",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@types/react": "^18.3.23",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.34.0",
|
||||
"@typescript-eslint/parser": "^8.34.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^9.1.0",
|
||||
"concurrently": "^9.1.2",
|
||||
"cpx2": "^8.0.0",
|
||||
"crypto-browserify": "^3.12.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.57.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"esbuild": "^0.25.5",
|
||||
"esbuild-sass-plugin": "^3.3.1",
|
||||
"eslint": "^9.29.0",
|
||||
"fs-extra": "^11.3.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.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.83.0",
|
||||
"sass-loader": "^16.0.4",
|
||||
"simple-git": "^3.27.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.7.2",
|
||||
"webpack": "^5.97.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
"sass": "^1.89.1",
|
||||
"simple-git": "^3.28.0",
|
||||
"ts-jest": "^29.3.4",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.17",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.7.5",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/node": "^18.19.68",
|
||||
"@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",
|
||||
"argparse": "^2.0.1",
|
||||
"compression": "^1.7.5",
|
||||
"compression": "^1.8.0",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.0.1",
|
||||
"express": "^5.1.0",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^5.0.3",
|
||||
"immutable": "^5.1.2",
|
||||
"io-ts": "^2.2.22",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.18.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"rxjs": "^7.8.2",
|
||||
"swagger-ui-dist": "^5.24.0",
|
||||
"tslib": "^2.8.1",
|
||||
"util.promisify": "^1.1.2",
|
||||
"xhr2": "^0.2.1"
|
||||
"util.promisify": "^1.1.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@google-cloud/storage": "^7.14.0",
|
||||
|
||||
307
scripts/build.mjs
Normal file
307
scripts/build.mjs
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Eric E <etongfu@@outlook.com>
|
||||
*/
|
||||
import * as esbuild from 'esbuild';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as argparse from 'argparse';
|
||||
import { sassPlugin } from 'esbuild-sass-plugin';
|
||||
import * as os from 'os';
|
||||
|
||||
const Apps = [
|
||||
// Apps
|
||||
{ kind: 'app', name: 'viewer' },
|
||||
{ kind: 'app', name: 'docking-viewer' },
|
||||
{ kind: 'app', name: 'mesoscale-explorer' },
|
||||
{ kind: 'app', name: 'mvs-stories', globalName: 'mvsStories', filename: 'mvs-stories.js' },
|
||||
|
||||
// Examples
|
||||
{ kind: 'example', name: 'proteopedia-wrapper' },
|
||||
{ kind: 'example', name: 'basic-wrapper' },
|
||||
{ kind: 'example', name: 'lighting' },
|
||||
{ kind: 'example', name: 'alpha-orbitals' },
|
||||
{ kind: 'example', name: 'alphafolddb-pae' },
|
||||
{ kind: 'example', name: 'mvs-stories' },
|
||||
{ kind: 'example', name: 'ihm-restraints' },
|
||||
{ kind: 'example', name: 'interactions' },
|
||||
{ kind: 'example', name: 'ligand-editor' },
|
||||
];
|
||||
|
||||
function findApp(name, kind) {
|
||||
return Apps.find(a => a.name === name && a.kind === kind);
|
||||
}
|
||||
|
||||
function mkDir(dir) {
|
||||
try {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to create directory ${dir}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
function handleFileError(error, operation, path) {
|
||||
console.error(`Failed to ${operation} ${path}:`, error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function fileLoaderPlugin(options) {
|
||||
mkDir(options.out);
|
||||
|
||||
return {
|
||||
name: 'file-loader',
|
||||
setup(build) {
|
||||
build.onLoad({ filter: /\.jpg$/ }, async (args) => {
|
||||
try {
|
||||
const name = path.basename(args.path);
|
||||
mkDir(path.resolve(options.out, 'images'));
|
||||
await fs.promises.copyFile(args.path, path.resolve(options.out, 'images', name));
|
||||
return {
|
||||
contents: `images/${name}`,
|
||||
loader: 'text',
|
||||
};
|
||||
} catch (error) {
|
||||
handleFileError(error, 'copy', args.path);
|
||||
}
|
||||
});
|
||||
build.onLoad({ filter: /\.(html|ico)$/ }, async (args) => {
|
||||
const name = path.basename(args.path);
|
||||
await fs.promises.copyFile(args.path, path.resolve(options.out, name));
|
||||
return {
|
||||
contents: '',
|
||||
loader: 'empty',
|
||||
};
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function examplesCssRenamePlugin({ root }) {
|
||||
return {
|
||||
name: 'example-css-rename',
|
||||
setup(build) {
|
||||
build.onEnd(async () => {
|
||||
if (fs.existsSync(path.resolve(root, 'index.css'))) {
|
||||
await fs.promises.rename(
|
||||
path.resolve(root, 'index.css'),
|
||||
path.resolve(root, 'molstar.css')
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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, entry, outfile } = getPaths(app);
|
||||
|
||||
const ctx = await esbuild.context({
|
||||
entryPoints: [entry],
|
||||
tsconfig: './tsconfig.json',
|
||||
bundle: true,
|
||||
minify: isProduction,
|
||||
minifyIdentifiers: false,
|
||||
sourcemap: includeSourceMap,
|
||||
globalName: app.globalName || 'molstar',
|
||||
outfile,
|
||||
plugins: [
|
||||
fileLoaderPlugin({ out: prefix }),
|
||||
sassPlugin({
|
||||
type: 'css',
|
||||
silenceDeprecations: ['import'],
|
||||
logger: {
|
||||
warn: (msg) => console.warn(msg),
|
||||
debug: () => { },
|
||||
}
|
||||
}),
|
||||
...(kind === 'example' ? [examplesCssRenamePlugin({ root: prefix })] : []),
|
||||
],
|
||||
external: ['crypto', 'fs', 'path', 'stream'],
|
||||
loader: {
|
||||
},
|
||||
color: true,
|
||||
logLevel: 'info',
|
||||
define: {
|
||||
'process.env.DEBUG': JSON.stringify(process.env.DEBUG || false),
|
||||
__MOLSTAR_PLUGIN_VERSION__: JSON.stringify(VERSION),
|
||||
__MOLSTAR_BUILD_TIMESTAMP__: `${TIMESTAMP}`,
|
||||
},
|
||||
});
|
||||
|
||||
await ctx.rebuild();
|
||||
|
||||
if (!isProduction) await ctx.watch();
|
||||
}
|
||||
|
||||
function findBrowserTests(names) {
|
||||
const dir = path.resolve('./src', 'tests', 'browser');
|
||||
let files = fs.readdirSync(dir).filter(file => file.endsWith('.ts')).map(file => file.replace('.ts', ''));
|
||||
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* 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.',
|
||||
required: false,
|
||||
nargs: '*',
|
||||
});
|
||||
argParser.add_argument('--examples', '-e', {
|
||||
help: 'Examples to build.',
|
||||
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,
|
||||
default: 1338,
|
||||
type: 'int',
|
||||
});
|
||||
|
||||
argParser.add_argument('--host', {
|
||||
help: 'Show all available host addresses.',
|
||||
required: false,
|
||||
action: 'store_true',
|
||||
});
|
||||
|
||||
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() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const ips = [];
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
// Skip internal and non-IPv4 addresses
|
||||
if (iface.internal || iface.family !== 'IPv4') continue;
|
||||
ips.push(iface.address);
|
||||
}
|
||||
}
|
||||
|
||||
return ips;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const promises = [];
|
||||
console.log(isProduction ? 'Building apps...' : '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);
|
||||
|
||||
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: ${protocol}://localhost:${args.port}`);
|
||||
if (args.host) {
|
||||
console.log('Available host addresses:');
|
||||
const ips = getLocalIPs();
|
||||
ips.forEach(ip => console.log(` ${protocol}://${ip}:${args.port}`));
|
||||
}
|
||||
console.log('');
|
||||
console.log('Watching for changes...');
|
||||
console.log('');
|
||||
console.log('Press Ctrl+C to stop.');
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* 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 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 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>`;
|
||||
|
||||
const manifestTag = /<!-- __MOLSTAR_MANIFEST__ -->/g;
|
||||
const manifestCode = `<link rel="manifest" href="./manifest.webmanifest">`;
|
||||
|
||||
const pwaTag = /<!-- __MOLSTAR_PWA__ -->/g;
|
||||
const pwaCode = `<script src='./pwa.js'></script>`;
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
console.log('\n###', command);
|
||||
@@ -31,70 +44,146 @@ function addAnalytics(path) {
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addManifest(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace(manifestTag, manifestCode);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addPwa(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace(pwaTag, pwaCode);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function addVersion(path) {
|
||||
const data = fs.readFileSync(path, 'utf8');
|
||||
const result = data.replace('__MOLSTAR_VERSION__', VERSION);
|
||||
fs.writeFileSync(path, result, 'utf8');
|
||||
}
|
||||
|
||||
function copyViewer() {
|
||||
console.log('\n###', 'copy viewer files');
|
||||
const viewerBuildPath = path.resolve(buildDir, '../build/viewer/');
|
||||
const viewerBuildPath = path.resolve(buildDir, 'viewer/');
|
||||
const viewerDeployPath = path.resolve(localPath, 'viewer/');
|
||||
fse.copySync(viewerBuildPath, viewerDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(viewerDeployPath, 'index.html'));
|
||||
addManifest(path.resolve(viewerDeployPath, 'index.html'));
|
||||
addPwa(path.resolve(viewerDeployPath, 'index.html'));
|
||||
|
||||
const pwaDataPath = path.resolve(dataDir, 'pwa/');
|
||||
fse.copySync(pwaDataPath, viewerDeployPath, { overwrite: true });
|
||||
addVersion(path.resolve(viewerDeployPath, 'sw.js'));
|
||||
}
|
||||
|
||||
function copyMe() {
|
||||
console.log('\n###', 'copy me files');
|
||||
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
|
||||
const meBuildPath = path.resolve(buildDir, 'mesoscale-explorer/');
|
||||
const meDeployPath = path.resolve(localPath, 'me/viewer/');
|
||||
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
|
||||
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, '../build/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, '../build/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);
|
||||
}
|
||||
@@ -27,7 +27,7 @@ import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import './index.html';
|
||||
import { ShowButtons, StructurePreset, ViewportComponent } from './viewport';
|
||||
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode } from '../../mol-util/debug';
|
||||
|
||||
@@ -25,7 +25,7 @@ import { MesoFocusLoci } from './behavior/camera';
|
||||
import { GraphicsMode, MesoscaleState } from './data/state';
|
||||
import { MesoSelectLoci } from './behavior/select';
|
||||
import { Transparency } from '../../mol-gl/webgl/render-item';
|
||||
import { LoadModel, loadExampleEntry, loadPdb, loadPdbDev, loadUrl, openState } from './ui/states';
|
||||
import { LoadModel, loadExampleEntry, loadPdb, loadPdbIhm, loadUrl, openState } from './ui/states';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
|
||||
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
|
||||
@@ -85,7 +85,7 @@ const DefaultMesoscaleExplorerOptions = {
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
viewportShowSettings: PluginConfig.Viewport.ShowSettings.defaultValue,
|
||||
viewportShowSelectionMode: false,
|
||||
viewportShowSelectionMode: true,
|
||||
viewportShowAnimation: false,
|
||||
viewportShowTrajectoryControls: false,
|
||||
pluginStateServer: PluginConfig.State.DefaultServer.defaultValue,
|
||||
@@ -120,8 +120,15 @@ export class MesoscaleExplorer {
|
||||
await loadPdb(this.plugin, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(id: string)} instead.
|
||||
*/
|
||||
async loadPdbDev(id: string) {
|
||||
await loadPdbDev(this.plugin, id);
|
||||
await this.loadPdbIhm(id);
|
||||
}
|
||||
|
||||
async loadPdbIhm(id: string) {
|
||||
await loadPdbIhm(this.plugin, id);
|
||||
}
|
||||
|
||||
static async create(elementOrId: string | HTMLElement, options: Partial<MesoscaleExplorerOptions> = {}) {
|
||||
@@ -143,7 +150,7 @@ export class MesoscaleExplorer {
|
||||
|
||||
PluginSpec.Behavior(MesoFocusLoci),
|
||||
PluginSpec.Behavior(MesoSelectLoci),
|
||||
|
||||
PluginSpec.Behavior(PluginBehaviors.Representation.SelectLoci),
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
],
|
||||
animations: [
|
||||
|
||||
@@ -73,7 +73,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
this.ctx.managers.interactivity.lociSelects.deselectAll();
|
||||
return;
|
||||
}
|
||||
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
|
||||
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : this.ctx.managers.interactivity.props.granularity);
|
||||
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
|
||||
@@ -116,7 +116,7 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
if (modifiers.control) {
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
|
||||
} else {
|
||||
const loci = Loci.normalize(current.loci, 'chain');
|
||||
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,12 +192,13 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
const t = isBinary ? d : utf8Read(d, 0, d.length);
|
||||
const file = Asset.File(new File([t], ent.file));
|
||||
|
||||
const color = ColorNames.skyblue;
|
||||
const color = (ent.color) ? Color.fromRgb(ent.color[0], ent.color[1], ent.color[2]) : ColorNames.skyblue;
|
||||
|
||||
const sizeFactor = ent.sizeFactor || 1;
|
||||
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
|
||||
const instances = ent.instances && getAssetInstances(ent.instances);
|
||||
const description = ent.description;
|
||||
|
||||
const label = ent.label || ent.file.split('.')[0];
|
||||
build = build
|
||||
.toRoot()
|
||||
@@ -300,6 +301,7 @@ type GenericEntity = {
|
||||
file: string
|
||||
label?: string
|
||||
description?: string
|
||||
color?: number[]
|
||||
groups: {
|
||||
/** reference to `${GenericGroup.id}` */
|
||||
id: string,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2023-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -12,7 +12,7 @@ import { Color } from '../../../mol-util/color';
|
||||
import { Spheres } from '../../../mol-geo/geometry/spheres/spheres';
|
||||
import { Clip } from '../../../mol-util/clip';
|
||||
import { escapeRegExp, stringToWords } from '../../../mol-util/string';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ParamMapping } from '../../../mol-util/param-mapping';
|
||||
import { EntityNode } from '../ui/entities';
|
||||
import { DistinctColorsProps, distinctColors } from '../../../mol-util/color/distinct';
|
||||
@@ -211,7 +211,8 @@ export function getClipObjects(values: SimpleClipProps, boundingSphere: Sphere3D
|
||||
invert: values.invert,
|
||||
position,
|
||||
scale,
|
||||
rotation: values.rotation
|
||||
rotation: values.rotation,
|
||||
transform: Mat4.identity(),
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -93,9 +93,15 @@
|
||||
return;
|
||||
}
|
||||
|
||||
var pdbihm = getParam('pdbihm', '[^&]+').trim();
|
||||
if (pdbihm) {
|
||||
me.loadPdbIhm(pdbihm);
|
||||
return;
|
||||
}
|
||||
// support for deprecated pdb-dev param
|
||||
var pdbdev = getParam('pdbdev', '[^&]+').trim();
|
||||
if (pdbdev) {
|
||||
me.loadPdbDev(pdbdev);
|
||||
me.loadPdbIhm(pdbdev);
|
||||
return;
|
||||
}
|
||||
window.addEventListener('unload', () => {
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
require('./style.scss');
|
||||
import './style.scss';
|
||||
export * from './app';
|
||||
|
||||
@@ -1,32 +1,28 @@
|
||||
@use "sass:color";
|
||||
|
||||
$default-background: #2D3E50;
|
||||
$font-color: #EDF1F2;
|
||||
$hover-font-color: #3B9AD9;
|
||||
$entity-current-font-color: #FFFFFF;
|
||||
$msp-btn-remove-background: #BF3A31;
|
||||
$msp-btn-remove-hover-font-color:#ffffff;
|
||||
$msp-btn-commit-on-font-color: #ffffff;
|
||||
$entity-badge-font-color: #ccd4e0;
|
||||
@use '../../mol-plugin-ui/skin/base/colors' with (
|
||||
$default-background: #2D3E50,
|
||||
$font-color: #EDF1F2,
|
||||
$hover-font-color: #3B9AD9,
|
||||
$entity-current-font-color: #FFFFFF,
|
||||
$msp-btn-remove-background: #BF3A31,
|
||||
$msp-btn-remove-hover-font-color:#ffffff,
|
||||
$msp-btn-commit-on-font-color: #ffffff,
|
||||
$entity-badge-font-color: #ccd4e0,
|
||||
|
||||
// used in LOG
|
||||
$log-message: #0CCA5D;
|
||||
$log-info: #5E3673;
|
||||
$log-warning: #FCC937;
|
||||
$log-error: #FD354B;
|
||||
// used in LOG
|
||||
$log-message: #0CCA5D,
|
||||
$log-info: #5E3673,
|
||||
$log-warning: #FCC937,
|
||||
$log-error: #FD354B,
|
||||
|
||||
$logo-background: rgba(0,0,0,0.75);
|
||||
$logo-background: rgba(0,0,0,0.75),
|
||||
|
||||
@function color-lower-contrast($color, $amount) {
|
||||
@return color.adjust($color, $lightness: -$amount, $space: hsl);
|
||||
}
|
||||
$color-adjust-sign: -1,
|
||||
);
|
||||
|
||||
@function color-increase-contrast($color, $amount) {
|
||||
@return color.adjust($color, $lightness: $amount, $space: hsl);
|
||||
}
|
||||
|
||||
@import 'mol-plugin-ui/skin/base/base';
|
||||
@import 'mol-plugin-ui/skin/base/variables';
|
||||
@use '../../mol-plugin-ui/skin/base/base';
|
||||
@use '../../mol-plugin-ui/skin/base/vars' as *;
|
||||
|
||||
a {
|
||||
color: $font-color;
|
||||
|
||||
@@ -29,6 +29,7 @@ import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { MesoFocusLoci } from '../behavior/camera';
|
||||
import Markdown from 'react-markdown';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { ColorLoaderControls } from './states';
|
||||
|
||||
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
|
||||
const { canvas3d } = plugin;
|
||||
@@ -619,6 +620,7 @@ class Node<P extends {}, S extends { isDisabled: boolean }> extends PluginUIComp
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean, action?: 'color' | 'clip' | 'root', isDisabled: boolean }> {
|
||||
state = {
|
||||
isCollapsed: !!this.props.cell.state.isCollapsed,
|
||||
@@ -881,12 +883,13 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const root = (isRoot && this.allGroups.length > 1) && <IconButton svg={BrushSvg} toggleState={false} disabled={disabled} small onClick={this.toggleRoot} />;
|
||||
const clip = <IconButton svg={ContentCutSvg} toggleState={false} disabled={disabled} small onClick={this.toggleClip} />;
|
||||
const visibility = <IconButton svg={state.isHidden ? VisibilityOffOutlinedSvg : VisibilityOutlinedSvg} toggleState={false} disabled={disabled} small onClick={this.toggleVisible} />;
|
||||
|
||||
const loadColorButton = (depth === 0) && <ColorLoaderControls plugin={this.plugin} />;
|
||||
return <>
|
||||
<div className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${depth * 10 + 5}px` }}>
|
||||
{expand}
|
||||
{label}
|
||||
{root || color}
|
||||
{loadColorButton}
|
||||
{clip}
|
||||
{visibility}
|
||||
</div>
|
||||
|
||||
@@ -24,9 +24,10 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { getAllEntities, getEntityLabel, MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
import { readFromFile } from '../../../mol-util/data-source';
|
||||
|
||||
function adjustPluginProps(ctx: PluginContext) {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
@@ -211,27 +212,53 @@ export async function loadPdb(ctx: PluginContext, id: string) {
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
|
||||
export async function loadPdbDev(ctx: PluginContext, id: string) {
|
||||
export async function loadPdbIhm(ctx: PluginContext, id: string) {
|
||||
await reset(ctx);
|
||||
let url: string;
|
||||
// 4 character PDB id, TODO: support extended PDB ID
|
||||
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`;
|
||||
url = `https://pdb-ihm.org/bcif/${id.toLowerCase()}.bcif`;
|
||||
} else {
|
||||
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
|
||||
url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
url = `https://pdb-ihm.org/bcif/${nId.toUpperCase()}.bcif`;
|
||||
}
|
||||
const data = await ctx.builders.data.download({ url, isBinary: true });
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
|
||||
async function loadColors(ctx: PluginContext, file: File) {
|
||||
const colorData = await ctx.runTask(readFromFile(file, 'json'));
|
||||
|
||||
const update = ctx.state.data.build();
|
||||
const allEntities = getAllEntities(ctx);
|
||||
|
||||
for (const entityCell of allEntities) {
|
||||
const label = getEntityLabel(ctx, entityCell);
|
||||
const tags = entityCell.transform.tags;
|
||||
const fullname = (tags?.[0].replace('comp:', '') ?? '') + '.' + label;
|
||||
// test each tag, siwtch to uniform color
|
||||
if (fullname in colorData) {
|
||||
const { x, y, z } = colorData[fullname];
|
||||
const color = Color.fromRgb(x, y, z);
|
||||
update.to(entityCell).update(old => {
|
||||
if (old.type) {
|
||||
old.colorTheme = { name: 'uniform', params: { value: color, lightness: old.colorTheme.params.lightness } };
|
||||
old.type.params.color = color;
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = color;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
await update.commit();
|
||||
}
|
||||
//
|
||||
|
||||
export const LoadDatabase = StateAction.build({
|
||||
display: { name: 'Database', description: 'Load from Database' },
|
||||
params: (a, ctx: PluginContext) => {
|
||||
return {
|
||||
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbDev: 'PDB-Dev' })),
|
||||
source: PD.Select('pdb', PD.objectToOptions({ pdb: 'PDB', pdbIhm: 'PDB-IHM' })),
|
||||
entry: PD.Text(''),
|
||||
};
|
||||
},
|
||||
@@ -239,8 +266,8 @@ export const LoadDatabase = StateAction.build({
|
||||
})(({ params }, ctx: PluginContext) => Task.create('Loading from database...', async taskCtx => {
|
||||
if (params.source === 'pdb') {
|
||||
await loadPdb(ctx, params.entry);
|
||||
} else if (params.source === 'pdbDev') {
|
||||
await loadPdbDev(ctx, params.entry);
|
||||
} else if (params.source === 'pdbIhm') {
|
||||
await loadPdbIhm(ctx, params.entry);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -299,7 +326,6 @@ export const LoadModel = StateAction.build({
|
||||
}
|
||||
}));
|
||||
|
||||
//
|
||||
|
||||
export class DatabaseControls extends PluginUIComponent {
|
||||
componentDidMount() {
|
||||
@@ -337,6 +363,30 @@ export class ExampleControls extends PluginUIComponent {
|
||||
}
|
||||
}
|
||||
|
||||
export function ColorLoaderControls({ plugin }: { plugin: PluginContext }) {
|
||||
const triggerLoadColors = () => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.accept = '.json';
|
||||
input.onchange = async (e) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || !input.files[0]) return;
|
||||
const file = input.files[0];
|
||||
await loadColors(plugin, new File([file], file.name));
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
svg={OpenInBrowserSvg}
|
||||
title="Load Colors"
|
||||
onClick={triggerLoadColors}
|
||||
small
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export async function openState(ctx: PluginContext, file: File) {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
delete customState.stateRef;
|
||||
@@ -426,7 +476,7 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
driver.setSteps([
|
||||
// Left panel
|
||||
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
|
||||
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-IHM databases.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
|
||||
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
|
||||
@@ -486,7 +536,6 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MesoQuickStylesControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
|
||||
39
src/apps/mvs-stories/context.ts
Normal file
39
src/apps/mvs-stories/context.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
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 }
|
||||
|
||||
|
||||
export class MVSStoriesContext {
|
||||
commands = new BehaviorSubject<MVSStoriesCommand | undefined>(undefined);
|
||||
state = {
|
||||
viewers: new BehaviorSubject<{ name?: string, model: MVSStoriesViewerModel }[]>([]),
|
||||
currentStoryData: new BehaviorSubject<string | Uint8Array | undefined>(undefined),
|
||||
isLoading: new BehaviorSubject(false),
|
||||
};
|
||||
|
||||
dispatch(command: MVSStoriesCommand) {
|
||||
this.commands.next(command);
|
||||
}
|
||||
|
||||
constructor(public name?: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getMVSStoriesContext(options?: { name?: string, container?: object }): MVSStoriesContext {
|
||||
const container: any = options?.container ?? window;
|
||||
container.componentContexts ??= {};
|
||||
const name = options?.name ?? '<default>';
|
||||
if (!container.componentContexts[name]) {
|
||||
container.componentContexts[name] = new MVSStoriesContext(options?.name);
|
||||
}
|
||||
return container.componentContexts[name];
|
||||
}
|
||||
2
src/apps/mvs-stories/elements/index.ts
Normal file
2
src/apps/mvs-stories/elements/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import './snapshot-markdown';
|
||||
import './viewer';
|
||||
129
src/apps/mvs-stories/elements/snapshot-markdown.tsx
Normal file
129
src/apps/mvs-stories/elements/snapshot-markdown.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, distinctUntilChanged, map } from 'rxjs';
|
||||
import { PluginComponent } from '../../../mol-plugin-state/component';
|
||||
import { getMVSStoriesContext, MVSStoriesContext } from '../context';
|
||||
import { MVSStoriesViewerModel } from './viewer';
|
||||
import { useBehavior } from '../../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { PluginStateSnapshotManager } from '../../../mol-plugin-state/manager/snapshots';
|
||||
import { PluginReactContext } from '../../../mol-plugin-ui/base';
|
||||
import { CSSProperties } from 'react';
|
||||
import { Markdown } from '../../../mol-plugin-ui/controls/markdown';
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownModel extends PluginComponent {
|
||||
readonly context: MVSStoriesContext;
|
||||
root: HTMLElement | undefined = undefined;
|
||||
|
||||
state = new BehaviorSubject<{
|
||||
entry?: PluginStateSnapshotManager.Entry,
|
||||
index?: number,
|
||||
all: PluginStateSnapshotManager.Entry[],
|
||||
}>({ all: [] });
|
||||
|
||||
get viewer() {
|
||||
return this.context.state.viewers.value?.find(v => this.options?.viewerName === v.name);
|
||||
}
|
||||
|
||||
sync() {
|
||||
const mng = this.viewer?.model.plugin?.managers.snapshot;
|
||||
this.state.next({
|
||||
entry: mng?.current,
|
||||
index: mng?.current ? mng?.getIndex(mng.current) : undefined,
|
||||
all: mng?.state.entries.toArray() ?? [],
|
||||
});
|
||||
}
|
||||
|
||||
async mount(root: HTMLElement) {
|
||||
this.root = root;
|
||||
|
||||
createRoot(root).render(<MVSStoriesSnapshotMarkdownUI model={this} />);
|
||||
|
||||
let currentViewer: MVSStoriesViewerModel | undefined = undefined;
|
||||
let sub: { unsubscribe: () => void } | undefined = undefined;
|
||||
this.subscribe(this.context.state.viewers.pipe(
|
||||
map(xs => xs.find(v => this.options?.viewerName === v.name)),
|
||||
distinctUntilChanged((a, b) => a?.model === b?.model)
|
||||
), viewer => {
|
||||
if (currentViewer !== viewer) {
|
||||
currentViewer = viewer?.model;
|
||||
sub?.unsubscribe();
|
||||
}
|
||||
if (!viewer) return;
|
||||
sub = this.subscribe(viewer.model.plugin?.managers.snapshot.events.changed, () => {
|
||||
this.sync();
|
||||
});
|
||||
this.sync();
|
||||
});
|
||||
|
||||
this.sync();
|
||||
}
|
||||
|
||||
constructor(private options?: { context?: { name?: string, container?: object }, viewerName?: string }) {
|
||||
super();
|
||||
|
||||
this.context = getMVSStoriesContext(options?.context);
|
||||
}
|
||||
}
|
||||
|
||||
export function MVSStoriesSnapshotMarkdownUI({ model }: { model: MVSStoriesSnapshotMarkdownModel }) {
|
||||
const state = useBehavior(model.state);
|
||||
const isLoading = useBehavior(model.context.state.isLoading);
|
||||
|
||||
const style: CSSProperties = { display: 'flex', flexDirection: 'column', height: '100%' };
|
||||
const className = 'mvs-stories-markdown-explanation';
|
||||
|
||||
if (isLoading) {
|
||||
return <div style={style} className={className}>
|
||||
<i>Loading...</i>
|
||||
</div>;
|
||||
}
|
||||
|
||||
if (state.all.length === 0) {
|
||||
return <div style={style} className={className}>
|
||||
<i>No snapshot loaded or no description available</i>
|
||||
</div>;
|
||||
}
|
||||
|
||||
return <div style={style} className={className}>
|
||||
<div style={{ display: 'flex', flexDirection: 'row', width: '100%', gap: '8px' }}>
|
||||
<span style={{ lineHeight: '38px', minWidth: 60, maxWidth: 60, flexShrink: 0 }}>{typeof state.index === 'number' ? state.index + 1 : '-'}/{state.all.length}</span>
|
||||
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(-1)} style={{ flexGrow: 1, flexShrink: 0 }}>Prev</button>
|
||||
<button onClick={() => model.viewer?.model.plugin?.managers.snapshot.applyNext(1)} style={{ flexGrow: 1, flexShrink: 0 }}>Next</button>
|
||||
</div>
|
||||
<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>{state.entry?.description ?? 'Description not available'}</Markdown>
|
||||
</PluginReactContext.Provider>
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
export class MVSStoriesSnapshotMarkdownViewer extends HTMLElement {
|
||||
private model: MVSStoriesSnapshotMarkdownModel | undefined = undefined;
|
||||
|
||||
async connectedCallback() {
|
||||
this.model = new MVSStoriesSnapshotMarkdownModel({
|
||||
context: { name: this.getAttribute('context-name') ?? undefined },
|
||||
viewerName: this.getAttribute('viewer-name') ?? undefined,
|
||||
});
|
||||
await this.model.mount(this);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.model?.dispose();
|
||||
this.model = undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('mvs-stories-snapshot-markdown', MVSStoriesSnapshotMarkdownViewer);
|
||||
131
src/apps/mvs-stories/elements/viewer.tsx
Normal file
131
src/apps/mvs-stories/elements/viewer.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
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';
|
||||
import { DefaultPluginUISpec } from '../../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../../mol-plugin/config';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../../mol-plugin/spec';
|
||||
import { getMVSStoriesContext, MVSStoriesContext } from '../context';
|
||||
|
||||
export class MVSStoriesViewerModel extends PluginComponent {
|
||||
readonly context: MVSStoriesContext;
|
||||
plugin?: PluginContext = undefined;
|
||||
|
||||
async mount(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
this.plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
showControls: false,
|
||||
controlsDisplay: 'landscape',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
viewport: {
|
||||
snapshotDescription: EmptyDescription,
|
||||
}
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
this.subscribe(this.context.commands, async (cmd) => {
|
||||
if (!cmd || !this.plugin) return;
|
||||
|
||||
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' }));
|
||||
loadedData = await loadMVSData(this.plugin, data, cmd.format ?? 'mvsj', { sourceUrl: cmd.url });
|
||||
} else if (cmd.data) {
|
||||
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);
|
||||
} else if (loadedData) {
|
||||
this.context.state.currentStoryData.next(JSON.stringify(loadedData));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
PluginCommands.Toast.Show(
|
||||
this.plugin,
|
||||
{ key: '<mvsload>', title: 'Error', message: e?.message ? `${e?.message}` : `${e}`, timeoutMs: 10000 }
|
||||
);
|
||||
} finally {
|
||||
this.context.state.isLoading.next(false);
|
||||
}
|
||||
});
|
||||
|
||||
const viewers = this.context.state.viewers.value;
|
||||
const next = [...viewers, { name: this.options?.name, model: this }];
|
||||
this.context.state.viewers.next(next);
|
||||
}
|
||||
|
||||
constructor(private options?: { context?: { name?: string, container?: object }, name?: string }) {
|
||||
super();
|
||||
|
||||
this.context = getMVSStoriesContext(options?.context);
|
||||
|
||||
const viewers = this.context.state.viewers.value;
|
||||
const index = viewers.findIndex(v => v.name === options?.name);
|
||||
if (index >= 0) {
|
||||
const next = [...viewers];
|
||||
next[index].model.dispose();
|
||||
next.splice(index, 0);
|
||||
this.context.state.viewers.next(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function EmptyDescription() {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
export class MVSStoriesViewer extends HTMLElement {
|
||||
private model: MVSStoriesViewerModel | undefined = undefined;
|
||||
|
||||
async connectedCallback() {
|
||||
this.model = new MVSStoriesViewerModel({
|
||||
name: this.getAttribute('name') ?? undefined,
|
||||
context: { name: this.getAttribute('context-name') ?? undefined },
|
||||
});
|
||||
await this.model.mount(this);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this.model?.dispose();
|
||||
this.model = undefined;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
window.customElements.define('mvs-stories-viewer', MVSStoriesViewer);
|
||||
BIN
src/apps/mvs-stories/favicon.ico
Normal file
BIN
src/apps/mvs-stories/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
121
src/apps/mvs-stories/index.html
Normal file
121
src/apps/mvs-stories/index.html
Normal file
@@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Molecular Stories</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 34%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 66%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16px;
|
||||
padding-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-left: none;
|
||||
background: #F6F5F3;
|
||||
z-index: -2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 40%;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 60%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.msp-viewport-controls-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="mvs-stories.css" />
|
||||
<script type="text/javascript" src="mvs-stories.js"></script>
|
||||
</head>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<div id="controls">
|
||||
<mvs-stories-snapshot-markdown context-name="story1" style="flex-grow: 1;" ></mvs-stories-snapshot-markdown>
|
||||
</div>
|
||||
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data">Download MVS State</a> | <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 format = urlParams.get('data-format');
|
||||
|
||||
// For testing purposes:
|
||||
// if (!storyUrl) {
|
||||
// storyUrl = 'https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/kinase-story.mvsj';
|
||||
// }
|
||||
|
||||
if (storyId) {
|
||||
mvsStories.loadFromID(storyId, { format: format || 'mvsj', contextName: 'story1' });
|
||||
} else if (storyUrl) {
|
||||
mvsStories.loadFromURL(storyUrl, { format: format || 'mvsj', contextName: 'story1' });
|
||||
}
|
||||
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
mvsStories.downloadCurrentStory({ contextName: 'story1' });
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
64
src/apps/mvs-stories/index.tsx
Normal file
64
src/apps/mvs-stories/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
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';
|
||||
import './styles.scss';
|
||||
import './index.html';
|
||||
|
||||
export function getContext(name?: string) {
|
||||
return getMVSStoriesContext({ name });
|
||||
}
|
||||
|
||||
export function loadFromURL(url: string, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
setTimeout(() => {
|
||||
getContext(options?.contextName).dispatch({
|
||||
kind: 'load-mvs',
|
||||
format: options?.format ?? 'mvsj',
|
||||
url,
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
export function loadFromData(data: MVSData | string | Uint8Array, options?: { format: 'mvsx' | 'mvsj', contextName?: string }) {
|
||||
setTimeout(() => {
|
||||
getContext(options?.contextName).dispatch({
|
||||
kind: 'load-mvs',
|
||||
format: options?.format ?? 'mvsj',
|
||||
data,
|
||||
});
|
||||
}, 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 };
|
||||
66
src/apps/mvs-stories/readme.md
Normal file
66
src/apps/mvs-stories/readme.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# MolViewSpec Stories App
|
||||
|
||||
An app that defines `mvs-stories-snapshot-markdown` and `mvs-stories-viewer` web components that can be used to view MolViewSpec molecular stories.
|
||||
|
||||
See the [mvs-stories](../../examples/mvs-stories) example that includes specific stories.
|
||||
|
||||
### Usage
|
||||
|
||||
- Get `mvs-stories.css` and `mvs-stories.js` from `build/mvs-stories` and include these to your HTML page
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="mvs-stories.css" />
|
||||
<script type="text/javascript" src="mvs-stories.js"></script>
|
||||
```
|
||||
|
||||
Can also use `https://cdn.jsdelivr.net/npm/molstar@latest/build/mvs-stories/mvs-stories.js` (and `.css`). `latest` can be substituted by specific version.
|
||||
|
||||
- Place the components in your page wrapper in `<div>` elements to set up positioning:
|
||||
|
||||
```html
|
||||
<div class="viewer">
|
||||
<mvs-stories-viewer />
|
||||
</div>
|
||||
<div class="snapshot">
|
||||
<mvs-stories-snapshot-markdown />
|
||||
</div>
|
||||
```
|
||||
|
||||
- Load MolViewSpec state:
|
||||
|
||||
```html
|
||||
<script>
|
||||
mvsStories.loadFromURL('https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj');
|
||||
</script>
|
||||
```
|
||||
|
||||
- See [index.html](./index.html) for full example of how to embed the app.
|
||||
|
||||
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
|
||||
|
||||
```bash
|
||||
npm run dev -- -a mvs-stories
|
||||
```
|
||||
|
||||
### Multiple Stories on a Single Page
|
||||
|
||||
To support multiple instances of stories, use the `context-name='unique-name'` attribute on the `mvs-` components together with `loadFromURL/Data(..., { contextName: 'unique-name' })`.
|
||||
|
||||
For example (simplified to not include layout):
|
||||
|
||||
```html
|
||||
<div>
|
||||
<mvs-stories-viewer context-name="1" />
|
||||
<mvs-stories-snapshot-markdown context-name="1" />
|
||||
</div>
|
||||
<div>
|
||||
<mvs-stories-viewer context-name="2" />
|
||||
<mvs-stories-snapshot-markdown context-name="2" />
|
||||
</div>
|
||||
|
||||
<script>
|
||||
mvsStories.loadFromURL('1.mvsj', { format: 'mvsj', contextName: '1' });
|
||||
mvsStories.loadFromURL('2.mvsj', { format: 'mvsj', contextName: '2' });
|
||||
</script>
|
||||
|
||||
```
|
||||
195
src/apps/mvs-stories/styles.scss
Normal file
195
src/apps/mvs-stories/styles.scss
Normal file
@@ -0,0 +1,195 @@
|
||||
@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;
|
||||
font-weight: 400;
|
||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
color: #222;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 1rem;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3rem;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2.6rem;
|
||||
line-height: 1.25;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 2rem;
|
||||
line-height: 1.3;
|
||||
letter-spacing: -.1rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.7rem;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -.08rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.4rem;
|
||||
line-height: 1.5;
|
||||
letter-spacing: -.05rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1.1rem;
|
||||
line-height: 1.6;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
button {
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 24px;
|
||||
color: #555;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 38px;
|
||||
letter-spacing: .1rem;
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: circle inside;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style: decimal inside;
|
||||
}
|
||||
|
||||
ol,
|
||||
ul {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul ul,
|
||||
ul ol,
|
||||
ol ol,
|
||||
ol ul {
|
||||
margin: 1.5rem 0 1.5rem 3rem;
|
||||
font-size: 90%;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: .2rem .5rem;
|
||||
margin: 0 .2rem;
|
||||
font-size: 90%;
|
||||
white-space: nowrap;
|
||||
background: #F1F1F1;
|
||||
border: 1px solid #E1E1E1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre>code {
|
||||
display: block;
|
||||
padding: 1rem 1.5rem;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 15px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #E1E1E1;
|
||||
}
|
||||
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
th:last-child,
|
||||
td:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
button,
|
||||
.button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select,
|
||||
fieldset {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote,
|
||||
dl,
|
||||
figure,
|
||||
table,
|
||||
p,
|
||||
ul,
|
||||
ol,
|
||||
form {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
.mvs-stories-markdown-explanation {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.mvs-stories-markdown-explanation h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2024 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>
|
||||
@@ -17,14 +17,13 @@ import { QualityAssessment } from '../../extensions/model-archive/quality-assess
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVSX } from '../../extensions/mvs/components/formats';
|
||||
import { loadMVSData, loadMVSX } from '../../extensions/mvs/components/formats';
|
||||
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
import { ZenodoImport } from '../../extensions/zenodo';
|
||||
@@ -58,16 +57,17 @@ import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { OpenFiles } from '../../mol-plugin-state/actions/file';
|
||||
import { StringLike } from '../../mol-io/common/string-like';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode, isProductionMode, isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
export const ExtensionMap = {
|
||||
'volseg': PluginSpec.Behavior(Volseg),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
|
||||
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
|
||||
@@ -121,7 +121,6 @@ const DefaultViewerOptions = {
|
||||
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
@@ -202,7 +201,6 @@ export class Viewer {
|
||||
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
|
||||
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
|
||||
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
|
||||
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
|
||||
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
@@ -288,14 +286,21 @@ export class Viewer {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Scheduled for removal in v5. Use {@link loadPdbIhm | loadPdbIhm(pdbIhm: string)} instead.
|
||||
*/
|
||||
loadPdbDev(pdbDev: string) {
|
||||
return this.loadPdbIhm(pdbDev);
|
||||
}
|
||||
|
||||
loadPdbIhm(pdbIhm: string) {
|
||||
const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
|
||||
source: {
|
||||
name: 'pdb-dev' as const,
|
||||
name: 'pdb-ihm' as const,
|
||||
params: {
|
||||
provider: {
|
||||
id: pdbDev,
|
||||
id: pdbIhm,
|
||||
encoding: 'bcif',
|
||||
},
|
||||
options: params.source.params.options,
|
||||
@@ -489,7 +494,8 @@ export class Viewer {
|
||||
: await plugin.builders.data.download({ url: params.model.url, isBinary: params.model.isBinary, label: params.modelLabel });
|
||||
|
||||
const provider = plugin.dataFormats.get(params.model.format);
|
||||
model = await provider!.parse(plugin, data);
|
||||
const parsed = await provider!.parse(plugin, data);
|
||||
model = parsed.topology;
|
||||
}
|
||||
|
||||
const data = params.coordinates.kind === 'coordinates-data'
|
||||
@@ -511,10 +517,10 @@ export class Viewer {
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
if (format === 'mvsj') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
const mvsData = MVSData.fromMVSJ(StringLike.toString(data));
|
||||
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: url, ...options });
|
||||
} else if (format === 'mvsx') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'binary' }));
|
||||
@@ -530,26 +536,24 @@ 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,'. */
|
||||
async loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
if (typeof data === 'string' && data.startsWith('base64')) {
|
||||
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
|
||||
}
|
||||
if (format === 'mvsj') {
|
||||
if (typeof data !== 'string') {
|
||||
data = new TextDecoder().decode(data); // Decode Uint8Array to string using UTF8
|
||||
}
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: undefined, ...options });
|
||||
} else if (format === 'mvsx') {
|
||||
if (typeof data === 'string') {
|
||||
throw new Error("loadMvsData: if `format` is 'mvsx', then `data` must be a Uint8Array or a base64-encoded string prefixed with 'base64,'.");
|
||||
}
|
||||
await this.plugin.runTask(Task.create('Load MVSX file', async ctx => {
|
||||
const parsed = await loadMVSX(this.plugin, ctx, data as Uint8Array);
|
||||
await loadMVS(this.plugin, parsed.mvsData, { sanityChecks: true, sourceUrl: parsed.sourceUrl, ...options });
|
||||
}));
|
||||
loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { appendSnapshots?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
|
||||
return loadMVSData(this.plugin, data, format, options);
|
||||
}
|
||||
|
||||
loadFiles(files: File[]) {
|
||||
const sessions = files.filter(f => {
|
||||
const fn = f.name.toLowerCase();
|
||||
return fn.endsWith('.molx') || fn.endsWith('.molj');
|
||||
});
|
||||
|
||||
if (sessions.length > 0) {
|
||||
return PluginCommands.State.Snapshots.OpenFile(this.plugin, { file: sessions[0] });
|
||||
} else {
|
||||
throw new Error(`Unknown MolViewSpec format: ${format}`);
|
||||
return this.plugin.runTask(this.plugin.state.data.applyAction(OpenFiles, {
|
||||
files: files.map(f => Asset.File(f)),
|
||||
format: { name: 'auto', params: {} },
|
||||
visuals: true
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -618,7 +622,7 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS },
|
||||
mvs: { MVSData, loadMVS, loadMVSData },
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<!-- __MOLSTAR_MANIFEST__ -->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
@@ -111,8 +112,11 @@
|
||||
var pdb = getParam('pdb', '[^&]+').trim();
|
||||
if (pdb) viewer.loadPdb(pdb);
|
||||
|
||||
var pdbIhm = getParam('pdb-ihm', '[^&]+').trim();
|
||||
if (pdbIhm) viewer.loadPdbIhm(pdbIhm);
|
||||
// support for deprecated pdb-dev param
|
||||
var pdbDev = getParam('pdb-dev', '[^&]+').trim();
|
||||
if (pdbDev) viewer.loadPdbDev(pdbDev);
|
||||
if (pdbDev) viewer.loadPdbIhm(pdbDev);
|
||||
|
||||
var emdb = getParam('emdb', '[^&]+').trim();
|
||||
if (emdb) viewer.loadEmdb(emdb);
|
||||
@@ -127,8 +131,12 @@
|
||||
// to aid GC
|
||||
viewer.dispose();
|
||||
});
|
||||
|
||||
const event = new CustomEvent("molstarViewerCreated", { detail: { viewer } });
|
||||
window.dispatchEvent(event);
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_PWA__ -->
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,5 +8,5 @@
|
||||
import './embedded.html';
|
||||
import './favicon.ico';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
export * from './app';
|
||||
|
||||
@@ -15,6 +15,7 @@ import { classifyFloatArray, classifyIntArray } from '../../mol-io/common/binary
|
||||
import { BinaryEncodingProvider } from '../../mol-io/writer/cif/encoder/binary';
|
||||
import { Category } from '../../mol-io/writer/cif/encoder';
|
||||
import { ReaderResult } from '../../mol-io/reader/result';
|
||||
import { utf8ReadLong } from '../../mol-io/common/utf8';
|
||||
|
||||
function showProgress(p: Progress) {
|
||||
process.stdout.write(`\r${new Array(80).join(' ')}`);
|
||||
@@ -31,14 +32,10 @@ async function readFile(ctx: RuntimeContext, filename: string): Promise<ReaderRe
|
||||
if (isGz) input = await unzipAsync(input);
|
||||
return await CIF.parseBinary(new Uint8Array(input)).runInContext(ctx);
|
||||
} else {
|
||||
let str: string;
|
||||
if (isGz) {
|
||||
const data = await unzipAsync(await readFileAsync(filename));
|
||||
str = data.toString('utf8');
|
||||
} else {
|
||||
str = await readFileAsync(filename, 'utf8');
|
||||
}
|
||||
return await CIF.parseText(str).runInContext(ctx);
|
||||
const data = isGz ? await unzipAsync(await readFileAsync(filename)) : await readFileAsync(filename);
|
||||
const str = utf8ReadLong(data);
|
||||
const cif = await CIF.parseText(str).runInContext(ctx);
|
||||
return cif;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ async function main(args: Args): Promise<void> {
|
||||
} else {
|
||||
throw new Error(`Input file name must end with .mvsj or .mvsx: ${input}`);
|
||||
}
|
||||
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: sourceUrl, extensions: args.no_extensions ? [] : undefined });
|
||||
await loadMVS(plugin, mvsData, { sanityChecks: true, sourceUrl: sourceUrl, extensions: args.no_extensions ? [] : undefined });
|
||||
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
if (args.molj) {
|
||||
|
||||
@@ -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(', ');
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,8 @@
|
||||
<div id="app"></div>
|
||||
<div id='controls'></div>
|
||||
<div id='sponsor'>
|
||||
<a href='https://www.entos.ai/envision' target="_blank" rel="noopener">
|
||||
<svg class="makeStyles-root-46" viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
|
||||
<a href='https://www.iambic-envision.com/' target="_blank" rel="noopener">
|
||||
<svg viewBox="0 0 190 36" xmlns="http://www.w3.org/2000/svg"><path d="M32.2591 28.6707C32.2591 32.3914 29.2421 35.407 25.5214 35.407C22.0752 35.407 19.2338 32.8206 18.8325 29.4831V29.4775C18.8143 29.3312 18.8018 29.1835 18.7934 29.0344C18.7934 29.0316 18.7921 29.0274 18.7921 29.0246V29.0177C18.7865 28.902 18.7837 28.7864 18.7837 28.6707C18.7837 26.2557 20.0532 24.1389 21.9609 22.9503C21.9623 22.9489 21.9651 22.9489 21.9665 22.9475C22.0933 22.8666 22.2243 22.7914 22.3581 22.7203C22.3581 22.7203 22.3595 22.7203 22.3595 22.7189C23.3029 22.2173 24.3787 21.933 25.5214 21.933C29.2421 21.933 32.2591 24.9486 32.2591 28.6707Z"></path><path d="M25.5214 14.0692C29.2421 14.0692 32.2591 11.0522 32.2591 7.33146C32.2591 3.61074 29.2421 0.59375 25.5214 0.59375C22.0529 0.59375 19.1962 3.21637 18.8255 6.58592C18.8185 6.67092 18.8116 6.75454 18.8018 6.83815C18.7893 7.00119 18.7837 7.16563 18.7837 7.33146C18.7837 9.73669 20.0434 11.8465 21.94 13.038C22.0891 13.116 22.2355 13.201 22.3776 13.2916C22.3783 13.2923 22.379 13.2926 22.3797 13.293C22.3804 13.2933 22.3811 13.2937 22.3818 13.2944C23.3196 13.7891 24.3871 14.0692 25.5214 14.0692Z"></path><path d="M19.3645 12.4113C20.2926 12.4113 21.1694 12.638 21.94 13.038C20.0434 11.8465 18.7837 9.73669 18.7837 7.33146C18.7837 7.16563 18.7893 7.00119 18.8018 6.83815C18.4688 9.76455 16.1385 12.0866 13.2065 12.4044C13.8545 13.1193 14.3785 13.9484 14.745 14.857C15.7497 13.3798 17.4443 12.4113 19.3645 12.4113Z"></path><path d="M14.7312 21.1249V21.1236C14.1279 20.2331 13.7767 19.1587 13.7767 18.0007V17.9728C13.7767 15.3084 12.2285 13.0035 9.9835 11.911C9.98141 11.9103 9.97967 11.9096 9.97793 11.9089C9.97619 11.9082 9.97444 11.9075 9.97235 11.9068C9.96817 11.904 9.96538 11.9026 9.9612 11.9012C9.95981 11.9012 9.95981 11.8998 9.95981 11.8998C9.9417 11.8915 9.92394 11.8831 9.90618 11.8747C9.8884 11.8664 9.87063 11.858 9.85251 11.8497C9.82046 11.8343 9.78701 11.819 9.75357 11.8051L9.74521 11.8009C8.91745 11.4372 8.0019 11.2351 7.03898 11.2351C3.31826 11.2351 0.30127 14.2521 0.30127 17.9728C0.30127 21.6935 3.31826 24.7105 7.03898 24.7105C7.98797 24.7105 8.89098 24.514 9.71037 24.1601C9.71246 24.1594 9.7142 24.1583 9.71594 24.1573C9.71768 24.1562 9.71943 24.1552 9.72152 24.1545C9.8107 24.1169 9.8985 24.0765 9.9849 24.0333L9.98629 24.0319C10.7625 23.6919 11.6181 23.5037 12.5197 23.5037C12.7524 23.5037 12.9824 23.5163 13.2081 23.54C13.2082 23.5399 13.2081 23.54 13.2081 23.54C15.0168 23.7365 16.5971 24.695 17.6185 26.0885C17.9195 25.1688 18.3752 24.3201 18.9563 23.5732C17.1964 23.4464 15.6635 22.5058 14.7312 21.1249Z"></path><g clip-path="url(#clip0)"><path d="M106.391 18.0021C106.391 11.3724 101.039 6 94.4389 6H88.4585C81.8581 6 76.5061 11.3724 76.5061 18.0021V30.0042H81.2845V18.0021C81.2845 14.0268 84.4941 10.8008 88.4585 10.8008H94.4347C98.395 10.8008 101.609 14.0226 101.609 18.0021V30.0042H106.391V18.0021Z"></path><path d="M149.432 6H142.258C135.653 6 130.301 11.3724 130.301 18.0021C130.301 24.6319 135.653 30.0042 142.258 30.0042H149.432C156.036 30.0042 161.388 24.6319 161.388 18.0021C161.388 11.3724 156.032 6 149.432 6ZM149.432 25.1992H142.258C138.297 25.1992 135.084 21.9774 135.084 17.9979C135.084 14.0183 138.293 10.7966 142.258 10.7966H149.432C153.392 10.7966 156.606 14.0183 156.606 17.9979C156.606 21.9774 153.392 25.1992 149.432 25.1992Z"></path><path d="M74.1151 25.1992H58.5736C55.4526 25.1992 52.804 23.1924 51.8171 20.3983H74.1151V17.9979C74.1151 17.1808 74.1868 16.3807 74.3175 15.5975H51.8171C52.804 12.8033 55.4526 10.7966 58.5736 10.7966H76.0383C77.1475 8.87458 78.6911 7.22773 80.5299 6H58.5736C51.969 6 46.6169 11.3724 46.6169 18.0021C46.6169 24.6276 51.969 30 58.5736 30H74.1151V25.1992Z"></path><path d="M120.74 6H115.958H102.369C104.212 7.22773 105.751 8.87458 106.861 10.8008H115.958V30H120.74V10.8008H129.838C130.947 8.87458 132.486 7.22773 134.329 6H120.74Z"></path><path d="M182.906 15.6017H169.756C168.436 15.6017 167.365 14.5264 167.365 13.2013C167.365 11.8762 168.436 10.8008 169.756 10.8008H188.882V6H169.756C165.796 6 162.582 9.22173 162.582 13.2013C162.582 17.1808 165.791 20.4025 169.756 20.4025H182.906C184.226 20.4025 185.297 21.4779 185.297 22.803C185.297 24.1281 184.226 25.2034 182.906 25.2034H161.852C160.743 27.1297 159.199 28.7765 157.361 30.0042H182.906C186.866 30.0042 190.08 26.7825 190.08 22.803C190.08 18.8234 186.866 15.6017 182.906 15.6017Z"></path></g><defs><clipPath id="clip0"><rect width="190" height="24" fill="white" transform="translate(0 6)"></rect></clipPath></defs></svg>
|
||||
<div>
|
||||
Entos Envision
|
||||
</div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { mountControls } from './controls';
|
||||
import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-asse
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { Model, ResidueIndex } from '../../mol-model/structure';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
export class AlphaFoldPAEExample {
|
||||
viewer: Viewer;
|
||||
@@ -46,7 +46,7 @@ export class AlphaFoldPAEExample {
|
||||
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
|
||||
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
|
||||
|
||||
createRoot(document.getElementById(this.plotContainerId)!).render(
|
||||
plotRoot.render(
|
||||
<div className='msp-plugin' style={{ background: 'white' }}>
|
||||
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isPositionLocation } from '../../mol-geo/util/location-iterator';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { ColorThemeCategory } from '../../mol-theme/color/categories';
|
||||
import { ThemeDataContext } from '../../mol-theme/theme';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
@@ -43,7 +44,7 @@ export function CustomColorTheme(
|
||||
export const CustomColorThemeProvider: ColorTheme.Provider<{}, 'basic-wrapper-custom-color-theme'> = {
|
||||
name: 'basic-wrapper-custom-color-theme',
|
||||
label: 'Custom Color Theme',
|
||||
category: ColorTheme.Category.Misc,
|
||||
category: ColorThemeCategory.Misc,
|
||||
factory: CustomColorTheme,
|
||||
getParams: () => ({}),
|
||||
defaultValues: { },
|
||||
|
||||
@@ -22,7 +22,7 @@ import { CustomToastMessage } from './controls';
|
||||
import { CustomColorThemeProvider } from './custom-theme';
|
||||
import './index.html';
|
||||
import { buildStaticSuperposition, dynamicSuperpositionTest, StaticSuperpositionTestData } from './superposition';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
|
||||
49
src/examples/ihm-restraints/index.html
Normal file
49
src/examples/ihm-restraints/index.html
Normal file
@@ -0,0 +1,49 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* IHM Restraints Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
|
||||
#links {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
font-family: sans-serif;
|
||||
font-size: 0.8rem;
|
||||
z-index: 2;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<div id="links">
|
||||
<a href="#" id="mvs-data" filename="ihm-restraints.mvsj">Download MVS State</a> | <a href="https://github.com/molstar/molstar/tree/master/src/examples/ihm-restraints" id="mvs-data" target="_blank" rel="noopener noreferrer">Source Code</a>
|
||||
</div>
|
||||
<script>
|
||||
loadIHMRestraints(document.getElementById('viewer')).then(state => {
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const data = JSON.stringify(state, null, 2);
|
||||
molStarDownload(new Blob([data], { type: 'application/json' }), 'ihm-restraints.mvsj');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
366
src/examples/ihm-restraints/index.tsx
Normal file
366
src/examples/ihm-restraints/index.tsx
Normal file
@@ -0,0 +1,366 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVS } from '../../extensions/mvs/load';
|
||||
import { MVSData_States, Snapshot } from '../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder } from '../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { parseCifText } from '../../mol-io/reader/cif/text/parser';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { trajectoryFromMmCIF } from '../../mol-model-formats/structure/mmcif';
|
||||
import { Model } from '../../mol-model/structure';
|
||||
import { CoarseElementKey, CoarseElementReference } from '../../mol-model/structure/model/properties/coarse';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ajaxGet } from '../../mol-util/data-source';
|
||||
import { download } from '../../mol-util/download';
|
||||
import './index.html';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
async function createViewer(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
const plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: true,
|
||||
showControls: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
[PluginConfig.Viewport.ShowTrajectoryControls, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
interface IHMRestraintInfo {
|
||||
e1: CoarseElementKey & { label_comp_id: string },
|
||||
e2: CoarseElementKey & { label_comp_id: string },
|
||||
a: Vec3,
|
||||
b: Vec3,
|
||||
restraintType: 'harmonic' | 'upper bound' | 'lower bound',
|
||||
threshold: number,
|
||||
satisfied: boolean,
|
||||
distance: number,
|
||||
}
|
||||
|
||||
interface IHMStructureInfo {
|
||||
entity_labels: [id: string | undefined, label: string | undefined][],
|
||||
model_restraints: IHMRestraintInfo[][],
|
||||
}
|
||||
|
||||
function getCoarseElementPosition(e: CoarseElementReference, model: Model, position: Vec3) {
|
||||
if (!e.kind) Vec3.set(position, 0, 0, 0);
|
||||
const { x, y, z } = model.coarseConformation[e.kind!];
|
||||
const idx = e.index;
|
||||
Vec3.set(position, x[idx], y[idx], z[idx]);
|
||||
}
|
||||
|
||||
const _elementRef = CoarseElementReference();
|
||||
function resolvePosition(model: Model, key: CoarseElementKey, position: Vec3) {
|
||||
if (model.coarseHierarchy.index.findElement(key, _elementRef)) {
|
||||
getCoarseElementPosition(_elementRef, model, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
const rI = model.atomicHierarchy.index.findResidueLabel(key);
|
||||
if (rI < 0) return false;
|
||||
|
||||
const atomStart = model.atomicHierarchy.residueAtomSegments.offsets[rI];
|
||||
const atomEnd = model.atomicHierarchy.residueAtomSegments.offsets[rI + 1];
|
||||
const atomId = model.atomicHierarchy.atoms.label_atom_id;
|
||||
let aI = atomStart;
|
||||
// Find CA otherwise use the first atom.
|
||||
// Possible future improvement: use the atom closest to the center of mass of the residue.
|
||||
for (; aI < atomEnd; aI++) {
|
||||
if (atomId.value(aI) === 'CA') break;
|
||||
}
|
||||
if (aI === atomEnd) aI = atomStart;
|
||||
|
||||
const { x, y, z } = model.atomicConformation;
|
||||
Vec3.set(position, x[aI], y[aI], z[aI]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const HarmonicRestraintTolerance = 0.1;
|
||||
|
||||
async function parseInfo(plugin: PluginContext, url: string): Promise<IHMStructureInfo> {
|
||||
const data = await plugin.runTask(ajaxGet(url)) as string;
|
||||
const parsed = await plugin.runTask(parseCifText(data));
|
||||
|
||||
if (parsed.isError) {
|
||||
console.error(parsed);
|
||||
return { entity_labels: [], model_restraints: [] };
|
||||
}
|
||||
|
||||
const trajectory = await plugin.runTask(trajectoryFromMmCIF(parsed.result.blocks[0], parsed.result));
|
||||
|
||||
const dataBlocks = parsed.result.blocks;
|
||||
|
||||
const ihm_cross_link_restraint = dataBlocks[0].categories['ihm_cross_link_restraint'];
|
||||
const entity_id_1 = ihm_cross_link_restraint.getField('entity_id_1')!;
|
||||
const asym_id_1 = ihm_cross_link_restraint.getField('asym_id_1')!;
|
||||
const seq_id_1 = ihm_cross_link_restraint.getField('seq_id_1')!;
|
||||
const comp_id_1 = ihm_cross_link_restraint.getField('comp_id_1')!;
|
||||
const entity_id_2 = ihm_cross_link_restraint.getField('entity_id_2')!;
|
||||
const asym_id_2 = ihm_cross_link_restraint.getField('asym_id_2')!;
|
||||
const seq_id_2 = ihm_cross_link_restraint.getField('seq_id_2')!;
|
||||
const comp_id_2 = ihm_cross_link_restraint.getField('comp_id_2')!;
|
||||
const restraint_type = ihm_cross_link_restraint.getField('restraint_type')!;
|
||||
const threshold = ihm_cross_link_restraint.getField('distance_threshold')!;
|
||||
|
||||
const e1key = CoarseElementKey();
|
||||
const e2key = CoarseElementKey();
|
||||
|
||||
const a = Vec3.zero();
|
||||
const b = Vec3.zero();
|
||||
|
||||
const entity_labels: IHMStructureInfo['entity_labels'] = [];
|
||||
const entity = dataBlocks[0].categories['entity'];
|
||||
const entity_id = entity.getField('id');
|
||||
const pdbx_description = entity.getField('pdbx_description');
|
||||
for (let i = 0; i < entity.rowCount; i++) {
|
||||
entity_labels.push([entity_id?.str(i), pdbx_description?.str(i)]);
|
||||
}
|
||||
|
||||
const model_restraints: IHMRestraintInfo[][] = [];
|
||||
|
||||
for (let modelIndex = 0; modelIndex < trajectory.frameCount; modelIndex++) {
|
||||
const _model = trajectory.getFrameAtIndex(modelIndex);
|
||||
const model = Task.is(_model) ? await plugin.runTask(_model) : _model;
|
||||
|
||||
const restraints: IHMRestraintInfo[] = [];
|
||||
model_restraints.push(restraints);
|
||||
|
||||
for (let i = 0; i < ihm_cross_link_restraint.rowCount; i++) {
|
||||
e1key.label_entity_id = entity_id_1.str(i);
|
||||
e1key.label_asym_id = asym_id_1.str(i);
|
||||
e1key.label_seq_id = seq_id_1.int(i);
|
||||
e2key.label_entity_id = entity_id_2.str(i);
|
||||
e2key.label_asym_id = asym_id_2.str(i);
|
||||
e2key.label_seq_id = seq_id_2.int(i);
|
||||
|
||||
if (!resolvePosition(model, e1key, a) || !resolvePosition(model, e2key, b)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const restraintType: 'harmonic' | 'upper bound' | 'lower bound' = restraint_type.str(i)?.toLowerCase() as any;
|
||||
const thresholdValue = threshold.float(i);
|
||||
const distance = Vec3.distance(a, b);
|
||||
|
||||
let satisfied = true;
|
||||
if (restraintType === 'harmonic') {
|
||||
const thresholdValue = threshold.float(i);
|
||||
satisfied = distance >= (1 - HarmonicRestraintTolerance) * thresholdValue && distance <= (1 + HarmonicRestraintTolerance) * thresholdValue;
|
||||
} else if (restraintType === 'upper bound') {
|
||||
satisfied = distance <= thresholdValue;
|
||||
} else if (restraintType === 'lower bound') {
|
||||
satisfied = distance >= thresholdValue;
|
||||
}
|
||||
|
||||
restraints.push({
|
||||
e1: { ...e1key, label_comp_id: comp_id_1.str(i) },
|
||||
e2: { ...e2key, label_comp_id: comp_id_2.str(i) },
|
||||
a: Vec3.clone(a),
|
||||
b: Vec3.clone(b),
|
||||
restraintType,
|
||||
threshold: thresholdValue,
|
||||
satisfied,
|
||||
distance,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return { entity_labels, model_restraints };
|
||||
}
|
||||
|
||||
function baseStructure(url: string, modelIndex: number, info: IHMStructureInfo, options?: { noEntityLabels?: boolean }) {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const structure = builder
|
||||
.download({ url })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure({ model_index: modelIndex });
|
||||
|
||||
structure
|
||||
.component({ selector: 'coarse' })
|
||||
.representation({ type: 'spacefill' })
|
||||
.color({ custom: { molstar_use_default_coloring: true } })
|
||||
.opacity({ opacity: 0.51 });
|
||||
|
||||
structure
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'cartoon' })
|
||||
.color({ custom: { molstar_use_default_coloring: true } })
|
||||
.opacity({ opacity: 0.51 });
|
||||
|
||||
if (!options?.noEntityLabels) {
|
||||
const primitives = structure.primitives();
|
||||
for (const [label_entity_id, text] of info.entity_labels) {
|
||||
if (!text) continue;
|
||||
primitives
|
||||
.label({ position: { label_entity_id }, text, label_size: 16, label_color: '#cccccc' });
|
||||
}
|
||||
}
|
||||
|
||||
return [builder, structure] as const;
|
||||
}
|
||||
|
||||
function drawConstraints([, structure]: ReturnType<typeof baseStructure>, restraints: IHMRestraintInfo[], options: {
|
||||
filter: (r: IHMRestraintInfo) => boolean,
|
||||
color: (r: IHMRestraintInfo) => any,
|
||||
radius?: (r: IHMRestraintInfo) => number,
|
||||
tooltip: (r: IHMRestraintInfo) => string | undefined,
|
||||
}) {
|
||||
const primitives = structure.primitives();
|
||||
for (const r of restraints) {
|
||||
if (!options.filter(r)) continue;
|
||||
|
||||
const radius = options.radius?.(r) ?? 1;
|
||||
|
||||
primitives.tube({
|
||||
start: r.a as any,
|
||||
end: r.b as any,
|
||||
color: options.color(r) || 'white',
|
||||
tooltip: options.tooltip(r),
|
||||
radius: radius,
|
||||
dash_length: radius,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function restraintTooltip(r: IHMRestraintInfo) {
|
||||
return `
|
||||
- Element 1: ${r.e1.label_entity_id} ${r.e1.label_asym_id} ${r.e1.label_seq_id} ${r.e1.label_comp_id}
|
||||
- Element 2: ${r.e2.label_entity_id} ${r.e2.label_asym_id} ${r.e2.label_seq_id} ${r.e2.label_comp_id}
|
||||
- Distance: ${r.distance.toFixed(2)} Å
|
||||
- Threshold: ${r.threshold.toFixed(2)} Å
|
||||
- Constraint: ${r.restraintType}
|
||||
- Satisfied: ${r.satisfied ? 'Yes' : 'No'}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function loadIHMRestraints(root: HTMLElement, url?: string) {
|
||||
url ??= 'https://pdb-ihm.org/cif/8zz1.cif';
|
||||
|
||||
const plugin = await createViewer(root);
|
||||
const info = await parseInfo(plugin, url);
|
||||
|
||||
const modelIndex = 0;
|
||||
const restraints = info.model_restraints[modelIndex];
|
||||
|
||||
const nVialoted = restraints.filter(r => !r.satisfied).length;
|
||||
const nSatisfied = restraints.length - nVialoted;
|
||||
|
||||
const snapshots: Snapshot[] = [];
|
||||
|
||||
let mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => true,
|
||||
color: r => r.e1.label_entity_id === r.e2.label_entity_id && r.e1.label_asym_id === r.e2.label_asym_id ? 'yellow' : 'blue',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'All Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### All Restraints
|
||||
|
||||
- Yellow: Intra-chain restraints
|
||||
- Blue: Inter-chain restraints
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => true,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Restraint Validation',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Restraint Validation
|
||||
|
||||
- Red: ${nVialoted} Violated restraints
|
||||
- Green: ${nSatisfied} Satisfied restraints
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => !r.satisfied,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Violated Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Violated Restraints
|
||||
|
||||
${nVialoted} restraints are violated.
|
||||
`,
|
||||
}));
|
||||
|
||||
mvs = baseStructure(url, modelIndex, info);
|
||||
drawConstraints(mvs, restraints, {
|
||||
filter: r => r.satisfied,
|
||||
color: r => r.satisfied ? 'green' : 'red',
|
||||
radius: r => 1,
|
||||
tooltip: restraintTooltip,
|
||||
});
|
||||
snapshots.push(mvs[0].getSnapshot({
|
||||
title: 'Satisfied Restraints',
|
||||
linger_duration_ms: 5000,
|
||||
description: `
|
||||
### Satisfied Restraints
|
||||
|
||||
${nSatisfied} restraints are satisfied.
|
||||
`,
|
||||
}));
|
||||
|
||||
const data: MVSData_States = {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'I/HM Restraints',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
|
||||
await loadMVS(plugin, data, { sanityChecks: true, keepCamera: true });
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
(window as any).loadIHMRestraints = loadIHMRestraints;
|
||||
(window as any).molStarDownload = download;
|
||||
35
src/examples/ihm-restraints/readme.md
Normal file
35
src/examples/ihm-restraints/readme.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# I/HM Restraints Example
|
||||
|
||||
This example illustrates:
|
||||
|
||||
- Using Mol* to parse CIF files and extract custom data
|
||||
- Using MolViewSpec to visualize I/HM structure along with annotated crosslink restraints
|
||||
|
||||
### Usage
|
||||
|
||||
- Clone Mol* GitHub repo and build it.
|
||||
```bash
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
```
|
||||
|
||||
- Get `molstar.css` and `index.js` from `build/examples/ihm-restraints` and include these to your HTML page in a similar fashion to [index.html](./index.html):
|
||||
|
||||
```html
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
...
|
||||
<div id="viewer"></div>
|
||||
...
|
||||
<script>
|
||||
loadIHMRestraints(document.getElementById('viewer'))
|
||||
</script>
|
||||
```
|
||||
|
||||
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
|
||||
|
||||
```bash
|
||||
npm run dev -- -e ihm-restraints
|
||||
```
|
||||
46
src/examples/interactions/index.html
Normal file
46
src/examples/interactions/index.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Interactions Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 20px;
|
||||
top: 20px;
|
||||
width: 640px;
|
||||
height: 480px;
|
||||
}
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 40%;
|
||||
bottom: 8px;
|
||||
font-family: sans-serif;
|
||||
padding: 8px;
|
||||
border: 1px dotted #aaa;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
#controls select {
|
||||
margin-left: 4px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<div id="controls">
|
||||
</div>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<script>
|
||||
initInteractionsExample('viewer', 'controls');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
382
src/examples/interactions/index.tsx
Normal file
382
src/examples/interactions/index.tsx
Normal file
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { InteractionElementSchema, InteractionKind, StructureInteractionElement, StructureInteractions } from '../../extensions/interactions/model';
|
||||
import { ComputeContacts, CustomInteractions, InteractionsShape } from '../../extensions/interactions/transforms';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { ResidueIndex, Structure, StructureElement, StructureProperties, StructureQuery } from '../../mol-model/structure';
|
||||
import { atoms } from '../../mol-model/structure/query/queries/generators';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { MultiStructureSelectionFromBundle, StructureSelectionFromBundle } from '../../mol-plugin-state/transforms/model';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { createPluginUI } from '../../mol-plugin-ui';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { renderReact18 } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import './index.html';
|
||||
import { Task } from '../../mol-task';
|
||||
import { computeContacts } from '../../extensions/interactions/compute';
|
||||
|
||||
async function createViewer(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
const plugin = await createPluginUI({
|
||||
target: root,
|
||||
render: renderReact18,
|
||||
spec: {
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: true,
|
||||
showControls: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
async function createBindingSiteRepresentation(plugin: PluginContext, interactions: StructureInteractions[], receptors: Map<string, Structure>) {
|
||||
const contactBundles = getBindingSiteBundles(interactions.flatMap(e => e.elements), receptors);
|
||||
const update = plugin.build();
|
||||
|
||||
for (const [ref, bundle] of contactBundles) {
|
||||
update.to(ref)
|
||||
.apply(StructureSelectionFromBundle, { bundle, label: 'Binding Site' })
|
||||
.apply(StructureRepresentation3D, {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.2 } },
|
||||
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
function getBindingSiteBundles(interactions: StructureInteractionElement[], receptors: Map<string, Structure>) {
|
||||
const residueIndices = new Map<string, Set<ResidueIndex>>();
|
||||
|
||||
const loc = StructureElement.Location.create();
|
||||
const add = (ref: string, loci: StructureElement.Loci) => {
|
||||
if (!receptors.has(ref)) return;
|
||||
|
||||
let set: Set<ResidueIndex>;
|
||||
if (residueIndices.has(ref)) {
|
||||
set = residueIndices.get(ref)!;
|
||||
} else {
|
||||
set = new Set<ResidueIndex>();
|
||||
residueIndices.set(ref, set);
|
||||
}
|
||||
StructureElement.Loci.forEachLocation(loci, l => {
|
||||
set.add(StructureProperties.residue.key(l));
|
||||
}, loc);
|
||||
};
|
||||
|
||||
for (const e of interactions) {
|
||||
add(e.aStructureRef!, e.a);
|
||||
add(e.bStructureRef!, e.b);
|
||||
}
|
||||
|
||||
const bundles: [ref: string, bundle: StructureElement.Bundle][] = [];
|
||||
|
||||
for (const [ref, indices] of Array.from(residueIndices.entries())) {
|
||||
if (indices.size === 0) continue;
|
||||
|
||||
const loci = StructureQuery.loci(
|
||||
atoms({
|
||||
residueTest: e => indices.has(StructureProperties.residue.key(e.element))
|
||||
}),
|
||||
receptors.get(ref)!,
|
||||
);
|
||||
if (StructureElement.Loci.isEmpty(loci)) continue;
|
||||
bundles.push([ref, StructureElement.Bundle.fromLoci(loci)]);
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function loadComputedExample(
|
||||
plugin: PluginContext,
|
||||
{ receptorUrl, ligandUrl }: { receptorUrl: [url: string, format: BuiltInTrajectoryFormat], ligandUrl: [url: string, format: BuiltInTrajectoryFormat] },
|
||||
options: { receptor_label_asym_id: string | undefined, analyzeTrajectory?: boolean }
|
||||
) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: receptorUrl[0] });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, receptorUrl[1]);
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default', { representationPreset: 'polymer-cartoon' });
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: ligandUrl[0] });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, ligandUrl[1]);
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.structure.ref!;
|
||||
const ligandRef = ligand?.structure.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
const interactionsRef = update.toRoot()
|
||||
.apply(MultiStructureSelectionFromBundle, {
|
||||
selections: [
|
||||
{ key: 'a', ref: receptorRef, bundle: StructureElement.Schema.toBundle(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id }) },
|
||||
{ key: 'b', ref: ligandRef, bundle: StructureElement.Schema.toBundle(ligand?.structure.data!, { }) },
|
||||
],
|
||||
isTransitive: true,
|
||||
label: 'Label'
|
||||
}, { dependsOn: refs })
|
||||
.apply(ComputeContacts);
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
if (!options.analyzeTrajectory) {
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.structure.data!]])
|
||||
);
|
||||
} else {
|
||||
const trajectoryInteractions: StructureInteractions[] = [];
|
||||
const receptorLoci = StructureElement.Schema.toLoci(receptor?.structure.data!, { label_asym_id: options.receptor_label_asym_id });
|
||||
for (let fI = 0; fI < ligandTrajectory.data!.frameCount; fI++) {
|
||||
const model = await Task.resolveInContext(ligandTrajectory.data!.getFrameAtIndex(fI));
|
||||
const structure = Structure.ofModel(model);
|
||||
const currentInteractions = await plugin.runTask(Task.create('Compute Contacts', ctx => {
|
||||
return computeContacts(ctx, [
|
||||
{ structureRef: receptorRef, loci: receptorLoci },
|
||||
{ structureRef: ligandRef, loci: Structure.toStructureElementLoci(structure) },
|
||||
]);
|
||||
}));
|
||||
trajectoryInteractions.push(currentInteractions);
|
||||
}
|
||||
|
||||
console.log('Interactions', trajectoryInteractions);
|
||||
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
trajectoryInteractions,
|
||||
new Map([[receptorRef, receptor?.structure.data!]])
|
||||
);
|
||||
}
|
||||
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async function loadCustomExample(plugin: PluginContext) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.representation.components.polymer.ref!;
|
||||
const ligandRef = ligand?.representation.components.all.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
|
||||
const interactionsRef = update.toRoot().apply(CustomInteractions, {
|
||||
interactions: [
|
||||
{
|
||||
kind: 'hydrogen-bond',
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 353, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: { atom_index: 9 },
|
||||
}
|
||||
]
|
||||
}, { dependsOn: refs });
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
|
||||
);
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTestAllExample(plugin: PluginContext) {
|
||||
await plugin.clear();
|
||||
|
||||
// Set up the receptor and ligand structures
|
||||
const receptorData = await plugin.builders.data.download({ url: '../../../examples/ace2.pdbqt' });
|
||||
const receptorTrajectory = await plugin.builders.structure.parseTrajectory(receptorData, 'pdbqt');
|
||||
const receptor = await plugin.builders.structure.hierarchy.applyPreset(receptorTrajectory, 'default');
|
||||
|
||||
const ligandData = await plugin.builders.data.download({ url: '../../../examples/ace2-hit.mol2' });
|
||||
const ligandTrajectory = await plugin.builders.structure.parseTrajectory(ligandData, 'mol2');
|
||||
const ligand = await plugin.builders.structure.hierarchy.applyPreset(ligandTrajectory, 'default', { representationPreset: 'atomic-detail' });
|
||||
|
||||
// Compute the interactions
|
||||
const update = plugin.build();
|
||||
|
||||
const receptorRef = receptor?.representation.components.polymer.ref!;
|
||||
const ligandRef = ligand?.representation.components.all.ref!;
|
||||
|
||||
const refs = [receptorRef, ligandRef];
|
||||
|
||||
const basic = (kind: InteractionKind, atom_index: number | number[], description?: string): InteractionElementSchema => {
|
||||
return {
|
||||
kind,
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 354, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: Array.isArray(atom_index) ? { items: { atom_index } } : { atom_index },
|
||||
description,
|
||||
};
|
||||
};
|
||||
|
||||
const covalent = (degree: number, atom_index: number): InteractionElementSchema => {
|
||||
return {
|
||||
kind: 'covalent',
|
||||
degree: degree === -1 ? 'aromatic' : Math.abs(degree) as 1 | 2 | 3 | 4,
|
||||
aStructureRef: receptorRef,
|
||||
a: { auth_seq_id: 354, auth_atom_id: 'N' },
|
||||
bStructureRef: ligandRef,
|
||||
b: { atom_index }
|
||||
};
|
||||
};
|
||||
|
||||
const interactionsRef = update.toRoot().apply(CustomInteractions, {
|
||||
interactions: [
|
||||
basic('unknown', 1),
|
||||
basic('ionic', 2),
|
||||
basic('pi-stacking', 3),
|
||||
basic('cation-pi', 4),
|
||||
basic('halogen-bond', 5),
|
||||
basic('hydrogen-bond', 6),
|
||||
basic('weak-hydrogen-bond', 7),
|
||||
basic('hydrophobic', 8),
|
||||
basic('metal-coordination', 9),
|
||||
covalent(1, 10),
|
||||
covalent(2, 11),
|
||||
covalent(3, 12),
|
||||
covalent(-1, 13), // aromatic
|
||||
basic('unknown', [0, 1, 2, 3, 13, 14], 'Testing centroid for atom set'),
|
||||
]
|
||||
}, { dependsOn: refs });
|
||||
|
||||
interactionsRef.apply(InteractionsShape).apply(ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
|
||||
console.log('Interactions', interactionsRef.selector.data?.interactions);
|
||||
|
||||
// Create ball and stick representations for the binding site and focus on the ligand
|
||||
await createBindingSiteRepresentation(
|
||||
plugin,
|
||||
[interactionsRef.selector.data?.interactions!],
|
||||
new Map([[receptorRef, receptor?.representation.components.polymer.data]])
|
||||
);
|
||||
PluginCommands.Camera.FocusObject(plugin, {
|
||||
targets: [{
|
||||
targetRef: ligand?.representation.representations.all.ref
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
const Examples = {
|
||||
'Computed (1iep)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['https://files.rcsb.org/download/1IEP.cif', 'mmcif'],
|
||||
ligandUrl: ['https://models.rcsb.org/v1/1iep/atoms?label_asym_id=G©_all_categories=false', 'mmcif']
|
||||
}, { receptor_label_asym_id: 'A' }),
|
||||
'Computed (ACE2)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['../../../examples/ace2.pdbqt', 'pdbqt'],
|
||||
ligandUrl: ['../../../examples/ace2-hit.mol2', 'mol2']
|
||||
}, { receptor_label_asym_id: 'B' }),
|
||||
'Computed (multiple)': (plugin: PluginContext) => loadComputedExample(plugin, {
|
||||
receptorUrl: ['../../../examples/docking/receptor_1.pdb', 'pdb'],
|
||||
ligandUrl: ['../../../examples/docking/ligands_1.sdf', 'sdf']
|
||||
}, { receptor_label_asym_id: undefined, analyzeTrajectory: true }),
|
||||
'Custom': loadCustomExample,
|
||||
'Synthetic': loadTestAllExample
|
||||
};
|
||||
|
||||
function SelectExampleUI({ state, load }: {
|
||||
state: BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>,
|
||||
load: (name: keyof typeof Examples) => void
|
||||
}) {
|
||||
const current = useBehavior(state);
|
||||
return <div>
|
||||
Select Example:{' '}
|
||||
<select value={current.name} onChange={e => load(e.target.value as any)} disabled={current.isLoading}>
|
||||
{Object.keys(Examples).map(k => <option key={k} value={k}>{k}</option>)}
|
||||
</select>
|
||||
</div>;
|
||||
}
|
||||
|
||||
async function init(viewer: HTMLElement | string, controls: HTMLElement | string, defaultExample: keyof typeof Examples = 'Computed (1iep)') {
|
||||
const root = typeof viewer === 'string' ? document.getElementById(viewer)! : viewer;
|
||||
const plugin = await createViewer(root);
|
||||
|
||||
const state = new BehaviorSubject<{ name?: keyof typeof Examples, isLoading?: boolean }>({});
|
||||
const loadExample = async (name: keyof typeof Examples) => {
|
||||
state.next({ name, isLoading: true });
|
||||
try {
|
||||
await Examples[name](plugin);
|
||||
state.next({ name });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
state.next({});
|
||||
}
|
||||
};
|
||||
|
||||
createRoot(
|
||||
typeof controls === 'string' ? document.getElementById(controls)! : controls
|
||||
).render(<SelectExampleUI state={state} load={loadExample} />);
|
||||
|
||||
loadExample(defaultExample);
|
||||
return { plugin, loadExample };
|
||||
}
|
||||
|
||||
|
||||
(window as any).initInteractionsExample = init;
|
||||
16
src/examples/interactions/readme.md
Normal file
16
src/examples/interactions/readme.md
Normal file
@@ -0,0 +1,16 @@
|
||||
# Interactions Example
|
||||
|
||||
An example showcasing features of the `interactions` extension, including:
|
||||
|
||||
- Computing interactions on demand using Mol*'s built-in interaction computation code
|
||||
- Triggering interaction computation separately for multiple ligands stored in a single SDF file
|
||||
- Specifying interaction manually from custom data source
|
||||
- Synthetic example showing all supported interaction types
|
||||
|
||||
To run development build locally from the root `molstar` directory (after `npm install`):
|
||||
|
||||
```bash
|
||||
npm run dev -- -e interactions
|
||||
```
|
||||
|
||||
and navigate to `build/examples/interactions` in the hosted server linked in the script output.
|
||||
219
src/examples/ligand-editor/edits.ts
Normal file
219
src/examples/ligand-editor/edits.ts
Normal file
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { JSONCifLigandGraph, JSONCifLigandGraphAtom, JSONCifLigandGraphBondProps } from '../../extensions/json-cif/ligand-graph';
|
||||
import { Quat, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { VdwRadius } from '../../mol-model/structure/model/properties/atomic';
|
||||
import { ElementSymbol } from '../../mol-model/structure/model/types';
|
||||
import { attachRGroup, RGroupName } from './r-groups';
|
||||
|
||||
export const TopologyEdits = {
|
||||
setElement: async (graph: JSONCifLigandGraph, atomIds: number[], type_symbol: string) => {
|
||||
for (const id of atomIds) {
|
||||
graph.modifyAtom(id, { type_symbol });
|
||||
}
|
||||
},
|
||||
addElement: async (graph: JSONCifLigandGraph, parentId: number, type_symbol: string) => {
|
||||
const p = graph.getAtom(parentId);
|
||||
if (!p) return;
|
||||
|
||||
const c = graph.getAtomCoords(p);
|
||||
const dir = approximateAddAtomDirection(graph, p);
|
||||
const r = 2 / 5 * (VdwRadius(ElementSymbol(p.row.type_symbol ?? 'C')) + VdwRadius(ElementSymbol(type_symbol)));
|
||||
const newAtom = graph.addAtom({
|
||||
...p.row,
|
||||
// NOTE: this is not correct for editing protein atoms
|
||||
// as they should have atom names from CCD, or at least the should be
|
||||
// unique. This should be fine for small ligand editing.
|
||||
auth_atom_id: type_symbol,
|
||||
label_atom_id: type_symbol,
|
||||
type_symbol,
|
||||
Cartn_x: c[0] + dir[0] * r,
|
||||
Cartn_y: c[1] + dir[1] * r,
|
||||
Cartn_z: c[2] + dir[2] * r
|
||||
});
|
||||
graph.addOrUpdateBond(p, newAtom, { value_order: 'sing', type_id: 'covale' });
|
||||
return newAtom;
|
||||
},
|
||||
removeAtoms: async (graph: JSONCifLigandGraph, atomIds: number[]) => {
|
||||
for (const id of atomIds) {
|
||||
graph.removeAtom(id);
|
||||
}
|
||||
},
|
||||
removeBonds: async (graph: JSONCifLigandGraph, atomIds: number[]) => {
|
||||
for (let i = 0; i < atomIds.length; ++i) {
|
||||
for (let j = i + 1; j < atomIds.length; ++j) {
|
||||
graph.removeBond(atomIds[i], atomIds[j]);
|
||||
}
|
||||
}
|
||||
},
|
||||
updateBonds: async (graph: JSONCifLigandGraph, atomIds: number[], props: JSONCifLigandGraphBondProps) => {
|
||||
// TODO: iterate on the all-pairs behavior
|
||||
// e.g. only add bonds if there is no path connecting them,
|
||||
// or by a distance threshold, ...
|
||||
for (let i = 0; i < atomIds.length; ++i) {
|
||||
for (let j = i + 1; j < atomIds.length; ++j) {
|
||||
graph.addOrUpdateBond(atomIds[i], atomIds[j], props);
|
||||
}
|
||||
}
|
||||
},
|
||||
attachRgroup: async (graph: JSONCifLigandGraph, atomId: number, name: RGroupName) => {
|
||||
await attachRGroup(graph, name, atomId);
|
||||
}
|
||||
};
|
||||
|
||||
export type GeometryEditFn = (param: number) => JSONCifLigandGraph;
|
||||
|
||||
export const GeometryEdits = {
|
||||
twist: (graph: JSONCifLigandGraph, atomIds: number[]): GeometryEditFn => {
|
||||
if (atomIds.length !== 2) {
|
||||
throw new Error('Twist requires exactly two atoms.');
|
||||
}
|
||||
|
||||
const { left, right } = splitGraph(graph, atomIds[0], atomIds[1]);
|
||||
|
||||
const active = left.length <= right.length ? left : right;
|
||||
const a = left.length <= right.length ? atomIds[0] : atomIds[1];
|
||||
const b = left.length <= right.length ? atomIds[1] : atomIds[0];
|
||||
|
||||
const pivot = graph.getAtomCoords(a);
|
||||
const axis = Vec3.sub(Vec3(), pivot, graph.getAtomCoords(b));
|
||||
Vec3.normalize(axis, axis);
|
||||
|
||||
const basePositions = active.map(a => graph.getAtomCoords(a));
|
||||
const xform = Quat();
|
||||
const p = Vec3();
|
||||
|
||||
return (angle: number) => {
|
||||
Quat.setAxisAngle(xform, axis, angle);
|
||||
for (let i = 0; i < active.length; ++i) {
|
||||
Vec3.copy(p, basePositions[i]);
|
||||
Vec3.sub(p, p, pivot);
|
||||
Vec3.transformQuat(p, p, xform);
|
||||
Vec3.add(p, p, pivot);
|
||||
graph.modifyAtom(active[i], {
|
||||
Cartn_x: p[0],
|
||||
Cartn_y: p[1],
|
||||
Cartn_z: p[2]
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
},
|
||||
stretch: (graph: JSONCifLigandGraph, atomIds: number[]): GeometryEditFn => {
|
||||
if (atomIds.length !== 2) {
|
||||
throw new Error('Stretch requires exactly two atoms.');
|
||||
}
|
||||
|
||||
const { left, right } = splitGraph(graph, atomIds[0], atomIds[1]);
|
||||
|
||||
const a = graph.getAtomCoords(atomIds[0]);
|
||||
const b = graph.getAtomCoords(atomIds[1]);
|
||||
const center = Vec3.add(Vec3(), b, a);
|
||||
Vec3.scale(center, center, 0.5);
|
||||
const baseDelta = Vec3.sub(Vec3(), a, center);
|
||||
const baseLeft = left.map(a => graph.getAtomCoords(a));
|
||||
const baseRight = right.map(a => graph.getAtomCoords(a));
|
||||
|
||||
const p = Vec3();
|
||||
const delta = Vec3();
|
||||
|
||||
return (factor: number) => {
|
||||
Vec3.scale(delta, baseDelta, factor);
|
||||
for (let i = 0; i < left.length; ++i) {
|
||||
Vec3.copy(p, baseLeft[i]);
|
||||
Vec3.add(p, p, delta);
|
||||
graph.modifyAtom(left[i], {
|
||||
Cartn_x: p[0],
|
||||
Cartn_y: p[1],
|
||||
Cartn_z: p[2]
|
||||
});
|
||||
}
|
||||
for (let i = 0; i < right.length; ++i) {
|
||||
Vec3.copy(p, baseRight[i]);
|
||||
Vec3.sub(p, p, delta);
|
||||
graph.modifyAtom(right[i], {
|
||||
Cartn_x: p[0],
|
||||
Cartn_y: p[1],
|
||||
Cartn_z: p[2]
|
||||
});
|
||||
}
|
||||
return graph;
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
function approximateAddAtomDirection(graph: JSONCifLigandGraph, parent: JSONCifLigandGraphAtom) {
|
||||
let deltas: Vec3[] = [];
|
||||
const bonds = graph.bondByKey.get(parent.key);
|
||||
if (!bonds?.length) return Vec3.create(1, 0, 0);
|
||||
|
||||
const c = graph.getAtomCoords(parent);
|
||||
for (const b of bonds) {
|
||||
const delta = Vec3.sub(Vec3(), graph.getAtomCoords(b.atom_2), c);
|
||||
deltas.push(delta);
|
||||
}
|
||||
|
||||
if (deltas.length === 1) {
|
||||
const ret = Vec3.negate(Vec3(), deltas[0]);
|
||||
Vec3.normalize(ret, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (deltas.length === 2) {
|
||||
const ret = Vec3.add(Vec3(), deltas[0], deltas[1]);
|
||||
Vec3.normalize(ret, ret);
|
||||
Vec3.negate(ret, ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Take the first three deltas and cross-product them
|
||||
deltas = deltas.slice(0, 3);
|
||||
const crossProducts: Vec3[] = [];
|
||||
for (let i = 0; i < deltas.length; ++i) {
|
||||
for (let j = i + 1; j < deltas.length; ++j) {
|
||||
const cross = Vec3.cross(Vec3(), deltas[i], deltas[j]);
|
||||
Vec3.normalize(cross, cross);
|
||||
crossProducts.push(cross);
|
||||
}
|
||||
}
|
||||
for (let i = 1; i < crossProducts.length; ++i) {
|
||||
Vec3.matchDirection(crossProducts[i], crossProducts[i], crossProducts[0]);
|
||||
}
|
||||
|
||||
const avg = Vec3.create(0, 0, 0);
|
||||
for (const cp of crossProducts) {
|
||||
Vec3.add(avg, avg, cp);
|
||||
}
|
||||
Vec3.normalize(avg, avg);
|
||||
return avg;
|
||||
}
|
||||
|
||||
function getAtomDepths(graph: JSONCifLigandGraph, atomId: number) {
|
||||
return graph.traverse(atomId, 'bfs', new Map<string, number>(), (a, depths, pred) => {
|
||||
depths.set(a.key, pred ? depths.get(pred.atom_1.key)! + 1 : 0);
|
||||
});
|
||||
}
|
||||
|
||||
function splitGraph(graph: JSONCifLigandGraph, leftId: number, rightId: number) {
|
||||
const xs = getAtomDepths(graph, leftId);
|
||||
const ys = getAtomDepths(graph, rightId);
|
||||
|
||||
const l: JSONCifLigandGraphAtom[] = [];
|
||||
const r: JSONCifLigandGraphAtom[] = [];
|
||||
for (const a of graph.atoms) {
|
||||
if (xs.has(a.key) && ys.has(a.key)) {
|
||||
if (xs.get(a.key)! < ys.get(a.key)!) l.push(a);
|
||||
else r.push(a);
|
||||
} else if (xs.has(a.key)) {
|
||||
l.push(a);
|
||||
} else if (ys.has(a.key)) {
|
||||
r.push(a);
|
||||
}
|
||||
}
|
||||
|
||||
return { left: l, right: r };
|
||||
}
|
||||
53
src/examples/ligand-editor/example-data.ts
Normal file
53
src/examples/ligand-editor/example-data.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export const ExampleMol = `2244
|
||||
-OEChem-04072009073D
|
||||
|
||||
21 21 0 0 0 0 0 0 0999 V2000
|
||||
1.2333 0.5540 0.7792 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.6952 -2.7148 -0.7502 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7958 -2.1843 0.8685 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.7813 0.8105 -1.4821 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.0857 0.6088 0.4403 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7927 -0.5515 0.1244 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7288 1.8464 0.4133 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1426 -0.4741 -0.2184 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0787 1.9238 0.0706 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7855 0.7636 -0.2453 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1409 -1.8536 0.1477 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.1094 0.6715 -0.3113 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.5305 0.5996 0.1635 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1851 2.7545 0.6593 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7247 -1.3605 -0.4564 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5797 2.8872 0.0506 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8374 0.8238 -0.5090 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7290 1.4184 0.8593 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.2045 0.6969 -0.6924 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7105 -0.3659 0.6426 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2555 -3.5916 -0.7337 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 5 1 0 0 0 0
|
||||
1 12 1 0 0 0 0
|
||||
2 11 1 0 0 0 0
|
||||
2 21 1 0 0 0 0
|
||||
3 11 2 0 0 0 0
|
||||
4 12 2 0 0 0 0
|
||||
5 6 1 0 0 0 0
|
||||
5 7 2 0 0 0 0
|
||||
6 8 2 0 0 0 0
|
||||
6 11 1 0 0 0 0
|
||||
7 9 1 0 0 0 0
|
||||
7 14 1 0 0 0 0
|
||||
8 10 1 0 0 0 0
|
||||
8 15 1 0 0 0 0
|
||||
9 10 2 0 0 0 0
|
||||
9 16 1 0 0 0 0
|
||||
10 17 1 0 0 0 0
|
||||
12 13 1 0 0 0 0
|
||||
13 18 1 0 0 0 0
|
||||
13 19 1 0 0 0 0
|
||||
13 20 1 0 0 0 0
|
||||
M END`;
|
||||
58
src/examples/ligand-editor/index.html
Normal file
58
src/examples/ligand-editor/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* Ligand Editor Example</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
#app {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
.editor-controls button {
|
||||
padding: 0 8px;
|
||||
background-color: transparent;
|
||||
color: black;
|
||||
border: 1px solid rgb(206, 200, 186);
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
height: 24px;
|
||||
}
|
||||
.editor-controls input[type="text"] {
|
||||
padding: 0 8px;
|
||||
border: 1px solid rgb(206, 200, 186);
|
||||
border-radius: 0;
|
||||
height: 24px;
|
||||
}
|
||||
.editor-controls textarea {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid rgb(206, 200, 186);
|
||||
border-radius: 0;
|
||||
}
|
||||
.editor-controls button:hover, .editor-controls input:hover, .editor-controls textarea:hover {
|
||||
background-color: rgba(206, 200, 186, 0.1);
|
||||
}
|
||||
.msp-selection-viewport-controls {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</div>
|
||||
<script type="text/javascript" src="./index.js"></script>
|
||||
<script>
|
||||
initLigandEditorExample('app');
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
433
src/examples/ligand-editor/index.tsx
Normal file
433
src/examples/ligand-editor/index.tsx
Normal file
@@ -0,0 +1,433 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BehaviorSubject, Subscription, throttleTime } from 'rxjs';
|
||||
import { JSONCifLigandGraph, JSONCifLigandGraphBondProps } from '../../extensions/json-cif/ligand-graph';
|
||||
import { JSONCifDataBlock, JSONCifFile } from '../../extensions/json-cif/model';
|
||||
import { ParseJSONCifFileData } from '../../extensions/json-cif/transformers';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { StructureElement, StructureProperties } from '../../mol-model/structure';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { ModelFromTrajectory, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { Plugin } from '../../mol-plugin-ui/plugin';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import { DefaultPluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
import { PluginSpec } from '../../mol-plugin/spec';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { GeometryEditFn, GeometryEdits, TopologyEdits } from './edits';
|
||||
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';
|
||||
|
||||
async function init(target: HTMLElement | string, molfile: string = ExampleMol) {
|
||||
const root = typeof target === 'string' ? document.getElementById(target)! : target;
|
||||
const plugin = await createViewer(root);
|
||||
const model = new EditorModel(plugin);
|
||||
createRoot(root).render(<AppUI model={model} />);
|
||||
loadMolfile(model, molfile);
|
||||
return model;
|
||||
}
|
||||
|
||||
(window as any).initLigandEditorExample = init;
|
||||
|
||||
async function createViewer(root: HTMLElement) {
|
||||
const spec = DefaultPluginUISpec();
|
||||
const plugin = new PluginUIContext({
|
||||
...spec,
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: false,
|
||||
showControls: false
|
||||
}
|
||||
},
|
||||
components: {
|
||||
remoteState: 'none',
|
||||
},
|
||||
behaviors: [
|
||||
...spec.behaviors,
|
||||
PluginSpec.Behavior(MolViewSpec)
|
||||
],
|
||||
config: [
|
||||
[PluginConfig.Viewport.ShowAnimation, false],
|
||||
[PluginConfig.Viewport.ShowSelectionMode, false],
|
||||
[PluginConfig.Viewport.ShowExpand, false],
|
||||
[PluginConfig.Viewport.ShowControls, false],
|
||||
]
|
||||
});
|
||||
await plugin.init();
|
||||
plugin.managers.interactivity.setProps({ granularity: 'element' });
|
||||
plugin.selectionMode = true;
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
async function loadMolfile(model: EditorModel, molfile: string) {
|
||||
const { plugin } = model;
|
||||
|
||||
await plugin.clear();
|
||||
|
||||
const file = await molfileToJSONCif(molfile);
|
||||
const update = plugin.build();
|
||||
const data = update.toRoot()
|
||||
.apply(ParseJSONCifFileData, { data: file.jsoncif });
|
||||
|
||||
data
|
||||
.apply(TrajectoryFromMmCif)
|
||||
.apply(ModelFromTrajectory)
|
||||
.apply(StructureFromModel, { type: { name: 'model', params: {} } })
|
||||
.apply(StructureRepresentation3D, {
|
||||
type: { name: 'ball-and-stick', params: {} },
|
||||
colorTheme: {
|
||||
name: 'element-symbol',
|
||||
params: { carbonColor: { name: 'element-symbol', params: {} } }
|
||||
}
|
||||
});
|
||||
|
||||
await update.commit();
|
||||
model.setDataSelector(data.selector);
|
||||
}
|
||||
|
||||
class EditorModel {
|
||||
private dataSelector: StateObjectSelector | undefined = undefined;
|
||||
|
||||
state = {
|
||||
element: new BehaviorSubject<string>('C'),
|
||||
history: new BehaviorSubject<JSONCifFile[]>([]),
|
||||
molfile: new BehaviorSubject<string>(''),
|
||||
};
|
||||
|
||||
get data() {
|
||||
return this.dataSelector?.cell?.transform?.params?.data as JSONCifFile | undefined;
|
||||
}
|
||||
|
||||
get history() {
|
||||
return this.state.history.value;
|
||||
}
|
||||
|
||||
createGraph() {
|
||||
return new JSONCifLigandGraph(this.data?.dataBlocks[0]!);
|
||||
}
|
||||
|
||||
setDataSelector(selector: StateObjectSelector) {
|
||||
this.dataSelector = selector;
|
||||
this.updateMolFile();
|
||||
}
|
||||
|
||||
updateMolFile() {
|
||||
if (!this.data) return this.state.molfile.next('');
|
||||
|
||||
try {
|
||||
const molfile = jsonCifToMolfile(this.data?.dataBlocks[0], {
|
||||
comment: 'Generated by Mol* Ligand Editor'
|
||||
});
|
||||
this.state.molfile.next(molfile);
|
||||
} catch (e) {
|
||||
console.error('Failed to convert to molfile');
|
||||
console.error(e);
|
||||
this.state.molfile.next(`Error: ${e}`);
|
||||
}
|
||||
}
|
||||
|
||||
async update(data: JSONCifDataBlock, pushHistory = true, historyData?: JSONCifFile) {
|
||||
if (!this.data) return;
|
||||
|
||||
const updated: JSONCifFile = {
|
||||
...this.data!,
|
||||
dataBlocks: [data],
|
||||
};
|
||||
|
||||
if (pushHistory) {
|
||||
this.state.history.next([...this.history, historyData ?? this.data!]);
|
||||
}
|
||||
const update = this.plugin.build();
|
||||
update.to(this.dataSelector!).update({ data: updated });
|
||||
await update.commit();
|
||||
|
||||
this.updateMolFile();
|
||||
}
|
||||
|
||||
undo = async () => {
|
||||
if (!this.dataSelector) return;
|
||||
if (this.history.length === 0) return;
|
||||
|
||||
const data = this.history[this.history.length - 1];
|
||||
this.state.history.next(this.history.slice(0, this.history.length - 1));
|
||||
|
||||
const update = this.plugin.build();
|
||||
update.to(this.dataSelector).update({ data });
|
||||
await update.commit();
|
||||
|
||||
this.updateMolFile();
|
||||
};
|
||||
|
||||
private getEditableStructures() {
|
||||
if (!this.dataSelector?.isOk) return new Set();
|
||||
|
||||
const structures = this.plugin.state.data.selectQ(q => q
|
||||
.byRef(this.dataSelector?.ref!)
|
||||
.subtree()
|
||||
.filter(c => PluginStateObject.Molecule.Structure.is(c.obj))
|
||||
);
|
||||
return new Set(structures.map(s => s.obj?.data));
|
||||
}
|
||||
|
||||
private getSelectedAtomIds() {
|
||||
if (!this.data) return [];
|
||||
|
||||
const structures = this.getEditableStructures();
|
||||
if (structures.size === 0) return [];
|
||||
|
||||
const { selection } = this.plugin.managers.structure;
|
||||
const ids: number[] = [];
|
||||
|
||||
selection.entries.forEach(e => {
|
||||
if (!structures.has(e.selection.structure)) return;
|
||||
StructureElement.Loci.forEachLocation(e.selection, (l) => {
|
||||
ids.push(StructureProperties.atom.id(l));
|
||||
});
|
||||
});
|
||||
return ids;
|
||||
}
|
||||
|
||||
async editGraphTopology<Args extends any[], T>(fn: (graph: JSONCifLigandGraph, ...args: Args) => Promise<T>, ...args: Args) {
|
||||
try {
|
||||
const graph = this.createGraph();
|
||||
const result = await fn(graph, ...args);
|
||||
const data = graph.getData().block;
|
||||
await this.update(data);
|
||||
this.plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
return result;
|
||||
} catch (e) {
|
||||
console.error('Failed to edit graph');
|
||||
console.error(e);
|
||||
this.notify(`${e}`, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private notify(message: string, timeoutMs = 2500) {
|
||||
PluginCommands.Toast.Show(this.plugin, { key: '<edit>', title: 'Edit', message, timeoutMs });
|
||||
}
|
||||
|
||||
setElement = async () => {
|
||||
const symbol = this.state.element.value.trim();
|
||||
if (!symbol) return this.notify('No element symbol provided');
|
||||
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (!ids.length) return this.notify('No atoms selected');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.setElement, ids, symbol);
|
||||
};
|
||||
|
||||
addElement = async () => {
|
||||
const symbol = this.state.element.value.trim();
|
||||
if (!symbol) return this.notify('No element symbol provided');
|
||||
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (ids.length !== 1) return this.notify('Select a single atom to add a new atom to');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.addElement, ids[0], symbol);
|
||||
};
|
||||
|
||||
removeAtoms = async () => {
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (!ids.length) return this.notify('No atoms selected');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.removeAtoms, ids);
|
||||
};
|
||||
|
||||
removeBonds = async () => {
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (!ids.length) return this.notify('No atoms selected');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.removeBonds, ids);
|
||||
};
|
||||
|
||||
updateBonds = async (props: JSONCifLigandGraphBondProps) => {
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (!ids.length) return this.notify('No atoms selected');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.updateBonds, ids, props);
|
||||
};
|
||||
|
||||
attachRgroup = async (name: RGroupName) => {
|
||||
const ids = this.getSelectedAtomIds();
|
||||
if (ids.length !== 1) return this.notify('Select a single hydrogen atom to attach an R-group to');
|
||||
|
||||
await this.editGraphTopology(TopologyEdits.attachRgroup, ids[0], name);
|
||||
};
|
||||
|
||||
private geometryEditInitialData: JSONCifFile | undefined = undefined;
|
||||
private geometryEditValues = new BehaviorSubject<[value: number, finish: boolean]>([0, false]);
|
||||
private currentGeometryEdit: GeometryEditFn | undefined = undefined;
|
||||
private currentGeomeryEditSub: Subscription | undefined = undefined;
|
||||
private geometryEditQueue = new SingleTaskQueue();
|
||||
|
||||
private applyGeometryEdit = ([param, finish]: [param: number, finish: boolean]) => {
|
||||
if (!this.currentGeometryEdit) return;
|
||||
const graph = this.currentGeometryEdit(param);
|
||||
const data = graph.getData().block;
|
||||
const initialData = this.geometryEditInitialData;
|
||||
if (finish) {
|
||||
this.currentGeometryEdit = undefined;
|
||||
this.currentGeomeryEditSub?.unsubscribe();
|
||||
this.currentGeomeryEditSub = undefined;
|
||||
this.geometryEditInitialData = undefined;
|
||||
}
|
||||
this.geometryEditQueue.run(() => this.update(data, finish, initialData));
|
||||
};
|
||||
|
||||
beginGeometryEdit<Args extends any[], T>(fn: (graph: JSONCifLigandGraph, ...args: Args) => GeometryEditFn, initial: number, ...args: Args) {
|
||||
try {
|
||||
this.geometryEditValues.next([initial, false]);
|
||||
const graph = this.createGraph();
|
||||
this.geometryEditInitialData = this.data!;
|
||||
this.currentGeometryEdit = fn(graph, ...args);
|
||||
this.currentGeomeryEditSub = this.geometryEditValues
|
||||
.pipe(throttleTime(1000 / 60, undefined, { leading: true, trailing: true }))
|
||||
.subscribe(this.applyGeometryEdit);
|
||||
} catch (e) {
|
||||
console.error('Failed to edit graph');
|
||||
console.error(e);
|
||||
this.notify(`${e}`, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
setGeometryEditValue(param: number, finish = false) {
|
||||
this.geometryEditValues.next([param, finish]);
|
||||
}
|
||||
|
||||
twist = () => {
|
||||
this.beginGeometryEdit(GeometryEdits.twist, 0, this.getSelectedAtomIds());
|
||||
};
|
||||
|
||||
stretch = () => {
|
||||
this.beginGeometryEdit(GeometryEdits.stretch, 0, this.getSelectedAtomIds());
|
||||
};
|
||||
|
||||
constructor(public plugin: PluginUIContext) { }
|
||||
}
|
||||
|
||||
function AppUI({ model }: { model: EditorModel }) {
|
||||
return <div style={{ display: 'flex', flexDirection: 'row', height: '100%', width: '100%' }}>
|
||||
<div style={{ flexGrow: 1, display: 'block', position: 'relative' }}>
|
||||
<Plugin plugin={model.plugin} />
|
||||
</div>
|
||||
<div style={{ flexShrink: 0, minWidth: 500, width: 400, display: 'flex', flexDirection: 'column', gap: '5px' }}>
|
||||
<ControlsUI model={model} />
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ControlsUI({ model }: { model: EditorModel }) {
|
||||
return <div style={{ display: 'flex', flexDirection: 'column', gap: '5px', padding: 8, overflow: 'hidden', overflowY: 'auto' }} className='editor-controls'>
|
||||
<div>
|
||||
<UndoButton model={model} />
|
||||
</div>
|
||||
<b>Atoms</b>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button onClick={model.removeAtoms}>Remove</button>
|
||||
<div>
|
||||
<ElementEditUI model={model} />
|
||||
<button onClick={model.setElement} style={{ borderLeft: 'none' }}>Set Element</button>
|
||||
<button onClick={model.addElement} style={{ borderLeft: 'none' }}>Add Element</button>
|
||||
</div>
|
||||
</div>
|
||||
<b>Bonds</b>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button onClick={model.removeBonds}>Remove</button>
|
||||
<button onClick={() => model.updateBonds({ value_order: 'sing', type_id: 'covale' })}>-</button>
|
||||
<button onClick={() => model.updateBonds({ value_order: 'doub', type_id: 'covale' })}>=</button>
|
||||
<button onClick={() => model.updateBonds({ value_order: 'trip', type_id: 'covale' })}>≡</button>
|
||||
</div>
|
||||
<b>R-groups</b>
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button onClick={() => model.attachRgroup('CH3')}>-CH<sub>3</sub></button>
|
||||
</div>
|
||||
<b>Geometry</b>
|
||||
<TwistUI model={model} />
|
||||
<StretchUI model={model} />
|
||||
<b>Molfile</b>
|
||||
<MolFileUI model={model} />
|
||||
</div>;
|
||||
}
|
||||
|
||||
function MolFileUI({ model }: { model: EditorModel }) {
|
||||
const molfile = useBehavior(model.state.molfile);
|
||||
return <>
|
||||
<textarea value={molfile} readOnly style={{ width: '100%', height: 200, fontFamily: 'monospace', fontSize: '10px' }} />
|
||||
<div style={{ display: 'flex', gap: '5px' }}>
|
||||
<button onClick={() => navigator.clipboard.writeText(molfile)}>Copy</button>
|
||||
<button onClick={() => download(new Blob([molfile], { type: 'text/plain' }), `edited-molecule-${Date.now()}.mol`)}>Save</button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
function UndoButton({ model }: { model: EditorModel }) {
|
||||
const history = useBehavior(model.state.history);
|
||||
return <button onClick={model.undo} disabled={history.length === 0}>Undo [{history.length}]</button>;
|
||||
}
|
||||
|
||||
function ElementEditUI({ model }: { model: EditorModel }) {
|
||||
const element = useBehavior(model.state.element);
|
||||
return <input type="text" value={element} style={{ width: 50 }} onChange={e => model.state.element.next(e.target.value)} />;
|
||||
}
|
||||
|
||||
const GeometryLabelWidth = 60;
|
||||
|
||||
function TwistUI({ model }: { model: EditorModel }) {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
return <div style={{ display: 'flex', alignItems: 'center', gap: 8 }} >
|
||||
<i style={{ width: GeometryLabelWidth }}>Twist</i> <input
|
||||
type='range' min={-60} max={60} step={1} value={value}
|
||||
onMouseDown={model.twist}
|
||||
onMouseUp={(e) => {
|
||||
requestAnimationFrame(() => {
|
||||
model.setGeometryEditValue(Math.PI * value / 60, true);
|
||||
setValue(0);
|
||||
});
|
||||
}}
|
||||
onChange={e => {
|
||||
const value = +e.target.value;
|
||||
setValue(value);
|
||||
model.setGeometryEditValue(Math.PI * value / 60);
|
||||
}}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function StretchUI({ model }: { model: EditorModel }) {
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
return <div style={{ display: 'flex', alignItems: 'center', gap: 8 }} >
|
||||
<i style={{ width: GeometryLabelWidth }}>Stretch</i> <input
|
||||
type='range' min={-60} max={60} step={1} value={value}
|
||||
onMouseDown={model.stretch}
|
||||
onMouseUp={(e) => {
|
||||
requestAnimationFrame(() => {
|
||||
model.setGeometryEditValue(0.5 * value / 60, true);
|
||||
setValue(0);
|
||||
});
|
||||
}}
|
||||
onChange={e => {
|
||||
const value = +e.target.value;
|
||||
setValue(value);
|
||||
model.setGeometryEditValue(0.5 * value / 60);
|
||||
}}
|
||||
/>
|
||||
</div>;
|
||||
}
|
||||
98
src/examples/ligand-editor/molfile.ts
Normal file
98
src/examples/ligand-editor/molfile.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { getJSONCifCategory, JSONCifDataBlock } from '../../extensions/json-cif/model';
|
||||
import { mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
|
||||
import { MolstarBondSiteSchema, MolstarBondSiteTypeId, MolstarBondSiteValueOrder } from '../../mol-model/structure/export/categories/molstar_bond_site';
|
||||
|
||||
function padLeft(v: any, n = 3) {
|
||||
let s = `${v}`;
|
||||
while (s.length < n) s = ' ' + s;
|
||||
return s;
|
||||
}
|
||||
|
||||
function padRight(v: any, n = 3) {
|
||||
let s = `${v}`;
|
||||
while (s.length < n) s = s + ' ';
|
||||
return s;
|
||||
}
|
||||
|
||||
function mapMolChage(v: number) {
|
||||
switch (v) {
|
||||
case 3: return 1;
|
||||
case 2: return 2;
|
||||
case 1: return 3;
|
||||
case -1: return 5;
|
||||
case -2: return 6;
|
||||
case -3: return 7;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function mapMolBondOrder(order: MolstarBondSiteValueOrder, type: MolstarBondSiteTypeId) {
|
||||
if (type !== 'covale') return 8;
|
||||
|
||||
switch (order) {
|
||||
case 'sing': return 1;
|
||||
case 'doub': return 2;
|
||||
case 'trip': return 3;
|
||||
case 'arom': return 4;
|
||||
default: return 8;
|
||||
}
|
||||
}
|
||||
|
||||
export function jsonCifToMolfile(data: JSONCifDataBlock, options?: { name?: string, comment?: string }) {
|
||||
// The method works in the sense that Mol* can re-open the file.
|
||||
// For production use, this will likely need more testing and tweaks (e.g., support for M CHG property).
|
||||
|
||||
if (data.categories.atom_site === undefined || data.categories.molstar_bond_site === undefined) {
|
||||
throw new Error('The data block must contain atom_site and molstar_bond_site categories.');
|
||||
}
|
||||
|
||||
const { atom_site: _atoms, molstar_bond_site: _bonds } = data.categories;
|
||||
|
||||
const atoms = getJSONCifCategory<mmCIF_Schema['atom_site']>(data, 'atom_site')!;
|
||||
const bonds = getJSONCifCategory<MolstarBondSiteSchema['molstar_bond_site']>(data, 'molstar_bond_site')!;
|
||||
|
||||
const lines = [
|
||||
`${options?.name ?? 'mol'}`,
|
||||
' Molstar 3D',
|
||||
options?.comment ?? '',
|
||||
`${padLeft(atoms.rows.length)}${padLeft(bonds.rows.length)} 0 0 0 0 0 0 0 0 V2000`,
|
||||
];
|
||||
|
||||
const atomIdToIndex = new Map<number, number>();
|
||||
for (let i = 0; i < atoms.rows.length; ++i) {
|
||||
const a = atoms.rows[i];
|
||||
const { id, Cartn_x, Cartn_y, Cartn_z, type_symbol, pdbx_formal_charge } = a;
|
||||
atomIdToIndex.set(id, i + 1);
|
||||
const fields = [
|
||||
padLeft(Cartn_x.toFixed(4), 10),
|
||||
padLeft(Cartn_y.toFixed(4), 10),
|
||||
padLeft(Cartn_z.toFixed(4), 10),
|
||||
' ',
|
||||
padRight(type_symbol, 2),
|
||||
' 0',
|
||||
padLeft(mapMolChage(pdbx_formal_charge), 3),
|
||||
' 0 0 0 0 0 0 0 0 0 0',
|
||||
];
|
||||
lines.push(fields.join(''));
|
||||
}
|
||||
|
||||
for (const b of bonds.rows) {
|
||||
const { atom_id_1, atom_id_2, value_order, type_id } = b;
|
||||
const fields = [
|
||||
padLeft(atomIdToIndex.get(atom_id_1)!, 3),
|
||||
padLeft(atomIdToIndex.get(atom_id_2)!, 3),
|
||||
padLeft(mapMolBondOrder(value_order, type_id), 3),
|
||||
' 0 0 0 0',
|
||||
];
|
||||
lines.push(fields.join(''));
|
||||
}
|
||||
|
||||
lines.push('M END');
|
||||
return lines.join('\n');
|
||||
}
|
||||
110
src/examples/ligand-editor/r-groups.ts
Normal file
110
src/examples/ligand-editor/r-groups.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { JSONCifLigandGraph, JSONCifLigandGraphAtom } from '../../extensions/json-cif/ligand-graph';
|
||||
import { molfileToJSONCif } from '../../extensions/json-cif/utils';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
|
||||
export type RGroupName = keyof typeof RGroups;
|
||||
|
||||
export async function attachRGroup(pGraph: JSONCifLigandGraph, rgroupName: RGroupName, pAtomOrId: number | JSONCifLigandGraphAtom) {
|
||||
const pAtom = pGraph.getAtom(pAtomOrId);
|
||||
if (pAtom?.row?.type_symbol !== 'H') {
|
||||
throw new Error('R-group attachment point must be a hydrogen atom.');
|
||||
}
|
||||
|
||||
const { molfile, jsoncif: rgroupData } = await molfileToJSONCif(RGroups[rgroupName]);
|
||||
const attachIdx = molfile.attachmentPoints?.[0].atomIdx;
|
||||
|
||||
if (typeof attachIdx !== 'number') {
|
||||
throw new Error('R-group attachment point not specified.');
|
||||
}
|
||||
|
||||
// Compute and apply rGroup transformation
|
||||
const pBonds = pGraph.getBonds(pAtom);
|
||||
if (pBonds.length !== 1) {
|
||||
throw new Error('R-group attachment point must have exactly 1 bond.');
|
||||
}
|
||||
const pDir = pGraph.getBondDirection(pBonds[0]);
|
||||
const pPivot = pBonds[0].atom_2;
|
||||
Vec3.negate(pDir, pDir);
|
||||
Vec3.normalize(pDir, pDir);
|
||||
|
||||
const rGraph = new JSONCifLigandGraph(rgroupData.dataBlocks[0]);
|
||||
const rAtom = rGraph.getAtomAtIndex(attachIdx - 1);
|
||||
|
||||
if (rAtom.row?.type_symbol !== 'R#') {
|
||||
throw new Error('R-group attachment point is not a R# atom.');
|
||||
}
|
||||
|
||||
const rCoords = rGraph.getAtomCoords(rAtom);
|
||||
const rBonds = rGraph.getBonds(rAtom);
|
||||
if (rBonds.length !== 1) {
|
||||
throw new Error('R-group R# atom must have exactly 1 bond.');
|
||||
}
|
||||
const rPivot = rGraph.getAtom(rBonds[0].atom_2);
|
||||
const rDir = rGraph.getBondDirection(rBonds[0]);
|
||||
Vec3.normalize(rDir, rDir);
|
||||
|
||||
const rotation = Vec3.makeRotation(Mat4(), rDir, pDir);
|
||||
const translation = Mat4.fromTranslation(Mat4(), Vec3.sub(Vec3(), pGraph.getAtomCoords(pPivot), rCoords));
|
||||
|
||||
const C = Mat4.fromTranslation(Mat4(), Vec3.negate(Vec3(), rCoords));
|
||||
const CT = Mat4.fromTranslation(Mat4(), rCoords);
|
||||
const T0 = Mat4.mul3(Mat4(), CT, rotation, C);
|
||||
const T = Mat4.mul(Mat4(), translation, T0);
|
||||
|
||||
rGraph.transformCoords(T);
|
||||
|
||||
// Merge the two graphs
|
||||
pGraph.removeAtom(pAtom);
|
||||
rGraph.removeAtom(rAtom);
|
||||
|
||||
const newAtomMap = new Map<string, JSONCifLigandGraphAtom>();
|
||||
|
||||
// Add atoms
|
||||
for (const a of rGraph.atoms) {
|
||||
const newAtom = pGraph.addAtom(a.row);
|
||||
newAtomMap.set(a.key, newAtom);
|
||||
if (a === rPivot) {
|
||||
pGraph.addOrUpdateBond(pPivot, newAtom, rBonds[0].props);
|
||||
}
|
||||
}
|
||||
|
||||
// Add bonds
|
||||
for (const a of rGraph.atoms) {
|
||||
if (a === rAtom) continue;
|
||||
const bonds = rGraph.getBonds(a);
|
||||
const atom1 = newAtomMap.get(a.key)!;
|
||||
for (const b of bonds) {
|
||||
if (b.atom_2 === rAtom) continue;
|
||||
const atom2 = newAtomMap.get(b.atom_2.key)!;
|
||||
pGraph.addOrUpdateBond(atom1, atom2, b.props);
|
||||
}
|
||||
}
|
||||
|
||||
return pGraph;
|
||||
}
|
||||
|
||||
// Assumes the "attachment point (M APO)" points to a hydrogen atom that gets removed
|
||||
// when the R-group is attached.
|
||||
const RGroups = {
|
||||
CH3: `CH3
|
||||
-OEChem-05072507373D
|
||||
|
||||
5 4 0 0 0 0 0 0 0999 V2000
|
||||
0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.5541 0.7996 0.4965 R# 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.6833 -0.8134 -0.2536 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7782 -0.3735 0.6692 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.4593 0.3874 -0.9121 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 2 1 0 0 0 0
|
||||
1 3 1 0 0 0 0
|
||||
1 4 1 0 0 0 0
|
||||
1 5 1 0 0 0 0
|
||||
M APO 1 2 1
|
||||
M END`
|
||||
};
|
||||
12
src/examples/ligand-editor/readme.md
Normal file
12
src/examples/ligand-editor/readme.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Ligand Editor Example
|
||||
|
||||
Basic small molecule editing features utilizing the `json-cif` format/extension.
|
||||
This application is (at least currently) not meant to be a production-ready molecule editor.
|
||||
|
||||
To run development build locally from the root `molstar` directory (after `npm install`):
|
||||
|
||||
```bash
|
||||
npm run dev -- -e ligand-editor
|
||||
```
|
||||
|
||||
and navigate to `build/examples/ligand-editor` in the hosted server linked in the script output.
|
||||
33
src/examples/ligand-editor/utils.ts
Normal file
33
src/examples/ligand-editor/utils.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
export class SingleTaskQueue {
|
||||
private queue: (() => Promise<void>)[] = [];
|
||||
|
||||
run(fn: () => Promise<void>) {
|
||||
if (this.queue.length < 2) {
|
||||
this.queue.push(fn);
|
||||
} else {
|
||||
this.queue[this.queue.length - 1] = fn;
|
||||
}
|
||||
if (this.queue.length === 1) {
|
||||
this.next();
|
||||
}
|
||||
}
|
||||
|
||||
private async next() {
|
||||
while (this.queue.length > 0) {
|
||||
try {
|
||||
const fn = this.queue[0];
|
||||
await fn();
|
||||
} catch (e) {
|
||||
console.error('Error in SingleTaskQueue execution:', e);
|
||||
} finally {
|
||||
this.queue.shift();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import './index.html';
|
||||
require('mol-plugin-ui/skin/light.scss');
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
|
||||
type LoadParams = { url: string, format?: BuiltInTrajectoryFormat, isBinary?: boolean, assemblyId?: string }
|
||||
|
||||
|
||||
BIN
src/examples/mvs-stories/favicon.ico
Normal file
BIN
src/examples/mvs-stories/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
124
src/examples/mvs-stories/index.html
Normal file
124
src/examples/mvs-stories/index.html
Normal file
@@ -0,0 +1,124 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Molecular Stories</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 34%;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 66%;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 16px;
|
||||
padding-bottom: 20px;
|
||||
border: 1px solid #ccc;
|
||||
border-left: none;
|
||||
background: #F6F5F3;
|
||||
z-index: -2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
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;
|
||||
}
|
||||
|
||||
@media (orientation:portrait) {
|
||||
#viewer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 40%;
|
||||
}
|
||||
|
||||
#controls {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 60%;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.msp-viewport-controls-buttons {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.select-story select {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
height: 38px;
|
||||
padding: 0 8px;
|
||||
color: #555;
|
||||
line-height: 38px;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: transparent;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #bbb;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
<script type="text/javascript" src="index.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="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>
|
||||
</div>
|
||||
<div id="links">
|
||||
<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>
|
||||
document.getElementById('mvs-data').addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
window.downloadStory();
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
window.initStories();
|
||||
}, 0);
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
|
||||
</html>
|
||||
95
src/examples/mvs-stories/index.tsx
Normal file
95
src/examples/mvs-stories/index.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { download } from '../../mol-util/download';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { Stories } from './stories';
|
||||
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { getMVSStoriesContext } from '../../apps/mvs-stories/context';
|
||||
import '../../apps/mvs-stories/elements';
|
||||
|
||||
import './favicon.ico';
|
||||
import '../../mol-plugin-ui/skin/light.scss';
|
||||
import '../../apps/mvs-stories/styles.scss';
|
||||
import './index.html';
|
||||
|
||||
function getContext(name?: string) {
|
||||
return getMVSStoriesContext({ name });
|
||||
}
|
||||
|
||||
type Story = { kind: 'built-in', id: string } | { kind: 'url', url: string, format: 'mvsx' | 'mvsj' } | undefined;
|
||||
const CurrentStory = new BehaviorSubject<Story>(undefined);
|
||||
|
||||
function SelectStoryUI({ subject }: { subject: BehaviorSubject<Story> }) {
|
||||
const current = useBehavior(subject);
|
||||
const selectedId = current?.kind === 'built-in' ? current.id : current?.kind === 'url' ? 'url' : '';
|
||||
|
||||
return <select onChange={e => {
|
||||
const value = e.currentTarget.value;
|
||||
const s = Stories.find(s => s.id === value);
|
||||
if (!s) return;
|
||||
subject.next({ kind: 'built-in', id: s.id });
|
||||
}} value={selectedId}>
|
||||
{!current && <option value=''>Select a story...</option>}
|
||||
{Stories.map(s => <option key={s.name} value={s.id}>Story: {s.name}</option>)}
|
||||
{current?.kind === 'url' && <option disabled>------------------</option>}
|
||||
{current?.kind === 'url' && <option value='url'>{current.url}</option>}
|
||||
</select>;
|
||||
}
|
||||
|
||||
function init() {
|
||||
CurrentStory.subscribe(story => {
|
||||
if (!story) {
|
||||
history.replaceState({}, '', '');
|
||||
} else if (story.kind === 'url') {
|
||||
history.replaceState({}, '', story ? `?story-url=${encodeURIComponent(story.url)}&data-format=${story.format}` : '');
|
||||
getContext().dispatch({
|
||||
kind: 'load-mvs',
|
||||
format: story.format,
|
||||
url: story.url,
|
||||
});
|
||||
} else if (story.kind === 'built-in') {
|
||||
history.replaceState({}, '', story ? `?story=${story.id}` : '');
|
||||
const s = Stories.find(s => s.id === story.id);
|
||||
if (s) {
|
||||
getContext().dispatch({
|
||||
kind: 'load-mvs',
|
||||
data: s.buildStory(),
|
||||
});
|
||||
} else {
|
||||
console.warn('Story not found:', story.id);
|
||||
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const storyUrl = urlParams.get('story-url');
|
||||
const dataFormat = urlParams.get('data-format') as 'mvsx' | 'mvsj' | null;
|
||||
const storyId = urlParams.get('story');
|
||||
|
||||
if (storyUrl) {
|
||||
CurrentStory.next({ kind: 'url', url: storyUrl, format: dataFormat ?? 'mvsj' });
|
||||
} else if (storyId) {
|
||||
CurrentStory.next({ kind: 'built-in', id: storyId });
|
||||
} else {
|
||||
CurrentStory.next({ kind: 'built-in', id: Stories[0].id });
|
||||
}
|
||||
|
||||
createRoot(document.getElementById('select-story')!).render(<SelectStoryUI subject={CurrentStory} />);
|
||||
}
|
||||
|
||||
(window as any).downloadStory = () => {
|
||||
if (CurrentStory.value?.kind !== 'built-in') return;
|
||||
const id = CurrentStory.value.id;
|
||||
const story = Stories.find(s => s.id === id);
|
||||
if (!story) return;
|
||||
const data = JSON.stringify(story.buildStory(), null, 2);
|
||||
download(new Blob([data], { type: 'application/json' }), `${id}-story.mvsj`);
|
||||
};
|
||||
(window as any).initStories = init;
|
||||
(window as any).CurrentStory = CurrentStory;
|
||||
23
src/examples/mvs-stories/readme.md
Normal file
23
src/examples/mvs-stories/readme.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# MolViewSpec Stories Example
|
||||
|
||||
This example illustrates using the `mvs-stories` app to tell molecular stories built with MolViewSpec.
|
||||
|
||||
See the [mvs-stories](../../apps/mvs-stories) app for more info about how to use this app separately.
|
||||
|
||||
### Usage
|
||||
|
||||
- Clone Mol* GitHub repo and build it.
|
||||
```bash
|
||||
git clone https://github.com/molstar/molstar.git
|
||||
cd molstar
|
||||
npm install
|
||||
npm build
|
||||
```
|
||||
|
||||
- See [index.html](./index.html) for example usage.
|
||||
|
||||
- For interactive development build (for production use `npm run build`) of the example that immediately reflects changes use:
|
||||
|
||||
```bash
|
||||
npm run dev -- -e mvs-stories
|
||||
```
|
||||
13
src/examples/mvs-stories/stories/index.ts
Normal file
13
src/examples/mvs-stories/stories/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { buildStory as kinase } from './kinase';
|
||||
import { buildStory as tbp } from './tbp';
|
||||
|
||||
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 },
|
||||
] as const;
|
||||
756
src/examples/mvs-stories/stories/kinase.ts
Normal file
756
src/examples/mvs-stories/stories/kinase.ts
Normal file
@@ -0,0 +1,756 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
import { MVSData_States } from '../../../extensions/mvs/mvs-data';
|
||||
import { createMVSBuilder, Structure as MVSStructure, Representation, Root } from '../../../extensions/mvs/tree/mvs/mvs-builder';
|
||||
import { MVSNodeParams } from '../../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
import { ColorT, ComponentExpressionT, isPrimitiveComponentExpressions, PrimitivePositionT } from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
|
||||
const Domains = {
|
||||
ChainA: { auth_asym_id: 'A' },
|
||||
|
||||
SH2: { auth_asym_id: 'A', beg_auth_seq_id: 146, end_auth_seq_id: 247 },
|
||||
SH3: { auth_asym_id: 'A', beg_auth_seq_id: 83, end_auth_seq_id: 145 },
|
||||
P_loop: { auth_asym_id: 'A', beg_auth_seq_id: 246, end_auth_seq_id: 255 },
|
||||
Activation_loop: { auth_asym_id: 'A', beg_auth_seq_id: 384, end_auth_seq_id: 402 },
|
||||
};
|
||||
|
||||
const DomainColors = {
|
||||
SH2: '#8ED1A4' as ColorT,
|
||||
SH2_BCR: '#D03B4B' as ColorT,
|
||||
SH3: '#64B9AA' as ColorT,
|
||||
P_loop: 'pink' as ColorT,
|
||||
Activation_loop: 'red' as ColorT,
|
||||
DFG_motif: 'orange' as ColorT,
|
||||
};
|
||||
|
||||
const Colors = {
|
||||
'1opl': '#4577B2' as ColorT,
|
||||
'2gqg': '#BC536D' as ColorT,
|
||||
'2g2i': '#BC536D' as ColorT,
|
||||
'1iep': '#B9E3A0' as ColorT,
|
||||
'3ik3': '#F3774B' as ColorT,
|
||||
'3oxz': '#7D7EA5' as ColorT,
|
||||
|
||||
'active-site': '#F3794C' as ColorT,
|
||||
'binding-site': '#FEEB9F' as ColorT,
|
||||
};
|
||||
|
||||
// Obtained using https://www.rcsb.org/alignment
|
||||
// Aligned to 1iep
|
||||
const Superpositions = {
|
||||
'1opl': [-0.6321036327, 0.3450463255, 0.6938213248, 0, -0.6288677634, -0.7515716885, -0.1991615756, 0, 0.4527364948, -0.5622126202, 0.6920597055, 0, 36.3924122492, 118.2516908402, -26.4992054179, 1] as unknown as Mat4,
|
||||
'3ik3': [-0.7767826245, -0.6295936551, 0.0148520572, 0, 0.6059737752, -0.7408035481, 0.2898376906, 0, -0.1714775143, 0.2341408391, 0.9569605684, 0, 21.0648276775, 53.0266628762, -0.3385906075, 1] as unknown as Mat4,
|
||||
'2gqg': [0.0648740828, -0.7163272638, 0.6947421137, 0, 0.0160329972, -0.6953706204, -0.7184724374, 0, 0.9977646498, 0.0577490387, -0.0336266582, 0, -31.0690973964, 146.0940883054, 39.7107422531, 1] as unknown as Mat4,
|
||||
'2g2i': [-0.5680242227, 0.6527660987, 0.5012433569, 0, -0.10067389, 0.5493518768, -0.8295042395, 0, -0.8168312251, -0.5216406194, -0.2463286704, 0, -8.1905690894, 75.7603329146, -6.1327389269, 1] as unknown as Mat4,
|
||||
'3oxz': [0.7989033646, 0.5984398921, -0.0601922711, 0, -0.1303123126, 0.269921501, 0.9540236289, 0, 0.5871729857, -0.754328893, 0.2936252816, 0, -8.0697093741, 58.1709160658, 19.0363028443, 1] as unknown as Mat4,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'A Kinase Out of Control',
|
||||
key: 'intro',
|
||||
description: `
|
||||
### The Structural Story of BCR-ABL: A Kinase Out of Control
|
||||
|
||||
BCR-ABL is a classic case of how structural biology can drive drug discovery. This story will help you understand:
|
||||
|
||||
- How the [ABL kinase is normally regulated](#regulated-kinase).
|
||||
- How a small genetic fusion creates a [rogue kinase](#rogue-kinase).
|
||||
- How ATP binding fuels [uncontrolled cancer growth](#unstoppable-signaling).
|
||||
- How [Imatinib revolutionized treatment](#imatinib) by locking the kinase in an inactive state.
|
||||
- How a [single mutation (T315I) enabled resistance](#mutation) and brought new challenges.
|
||||
- How [Ponatinib](#ponatinib) and future inhibitors are being designed to keep up in this ongoing battle.
|
||||
`,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly,] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
_1opl_poly.label({ text: 'ABL Kinase' });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [103.72, 69.35, 20.52],
|
||||
target: [0.36, 55.32, 21.8],
|
||||
up: [-0.01, 0.01, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The ABL Kinase: A Well-Regulated Enzyme',
|
||||
key: 'regulated-kinase',
|
||||
description: `
|
||||
### The ABL Kinase: A Well-Regulated Enzyme
|
||||
|
||||
Normally, the ABL kinase ([PDB ID 1OPL](${wwPDBLink('1opl')})) is a well-regulated enzyme, kept in check by its SH3 and SH2 domains which fold back onto the kinase domain like a safety lock.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
domains(_1opl, _1opl_poly_repr, [
|
||||
[Domains.SH2, DomainColors.SH2, 'SH2'],
|
||||
[Domains.SH3, DomainColors.SH3, 'SH3'],
|
||||
], { label_size: 9 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-18.33, -30.35, 48.2],
|
||||
target: [-10.37, 49.7, 12.68],
|
||||
up: [-0.27, -0.37, -0.89],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The Birth of a Rogue Kinase',
|
||||
key: 'rogue-kinase',
|
||||
transition_duration_ms: 750,
|
||||
description: `
|
||||
### The Birth of a Rogue Kinase
|
||||
|
||||
But in BCR-ABL, this safety mechanism is gone. A reciprocal translocation between chromosomes 9 and 22 creates the Philadelphia chromosome (Ph),
|
||||
fusing the ABL1 gene from chromosome 9 with the BCR gene on chromosome 22. This fusion produces the chimeric protein, BCR-ABL, which lacks the
|
||||
regulation of the wildtype protein. Read more about this [here](https://www.cancer.gov/publications/dictionaries/cancer-terms/def/philadelphia-chromosome)
|
||||
and [the history of its discovery](https://pmc.ncbi.nlm.nih.gov/articles/PMC1934591/).
|
||||
|
||||
Comparing the normal protein to the kinase domain alone ([PDB ID 2GQG](${wwPDBLink('2gqg')}), in light red), you can
|
||||
see how the SH3 and SH2 domains (teal in normal ABL, red in BCR-ABL, with SH3 domain being unresolved in the crystal structure) are no longer positioned to restrain the kinase.
|
||||
|
||||
With this lock removed, BCR-ABL is stuck in an active conformation, like an accelerator pedal jammed to the floor. Without
|
||||
its normal regulation, BCR-ABL will keep signaling, unchecked causing unregulated cell growth and cancer — [chronic myeloid leukemia (CML)](https://en.wikipedia.org/wiki/Chronic_myelogenous_leukemia).
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1opl = structure(builder, '1opl');
|
||||
const [_1opl_poly, _1opl_poly_repr] = polymer(_1opl, { color: Colors['1opl'] });
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'C' },
|
||||
uniform_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
ligand(_1opl, {
|
||||
selector: { label_asym_id: 'D' },
|
||||
surface: true,
|
||||
carbon_color: Colors['1opl'],
|
||||
});
|
||||
|
||||
domains(_1opl, _1opl_poly_repr, [
|
||||
[Domains.SH2, DomainColors.SH2, 'SH2'],
|
||||
[Domains.SH3, DomainColors.SH3, 'SH3'],
|
||||
], { label_size: 9 });
|
||||
|
||||
const _2gqg = structure(builder, '2gqg');
|
||||
const [, _2gqg_poly_repr] = polymer(_2gqg, { color: '#BF99A1' });
|
||||
|
||||
domains(_2gqg, _2gqg_poly_repr, [
|
||||
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2 (BCR)'],
|
||||
], { label_size: 6 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [30.7, -18.5, 13.47],
|
||||
target: [3.99, 47.45, 0.08],
|
||||
up: [-0.22, -0.28, -0.94],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'ATP Binding and Unstoppable Signaling [1/2]',
|
||||
key: 'unstoppable-signaling',
|
||||
description: `
|
||||
### ATP Binding and Unstoppable Signaling
|
||||
|
||||
To function, every kinase needs [ATP](https://en.wikipedia.org/wiki/Kinase), and BCR-ABL is no exception. ATP donates a phosphate group that is transferred to a substrate
|
||||
during phosphorylation — a key step in signaling pathways that control cell growth. However, ATP is chemically unstable under the conditions
|
||||
used for crystallography. It often breaks down into ADP (adenosine diphosphate), losing one of its three phosphate
|
||||
groups — the very group that would normally be transferred during catalysis.
|
||||
|
||||
Because of this instability, we don't have crystal structures of BCR-ABL bound to ATP itself. Instead, researchers have studied
|
||||
the H396P mutant, which locks the kinase in a permanently active conformation, to understand how it binds nucleotides.
|
||||
In this structure, [ADP](https://www.ebi.ac.uk/pdbe-srv/pdbechem/chemicalCompound/show/ADP) is clearly nestled in the kinase's active site.
|
||||
|
||||
Key catalytic residues — Lys271, Glu286, and Asp381 (in orange) — form a highly conserved network that helps position and stabilize the nucleotide.
|
||||
Glu286, in particular, forms a salt bridge with Lys271, anchoring the active site in a catalytically competent conformation. This arrangement
|
||||
supports efficient phosphate transfer, which is central to BCR-ABL's ability to activate downstream signaling pathways.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _2g2i = structure(builder, '2g2i');
|
||||
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
|
||||
|
||||
ligand(_2g2i, {
|
||||
selector: { label_asym_id: 'E' },
|
||||
surface: true,
|
||||
label: 'ADP',
|
||||
label_size: 2,
|
||||
label_color: Colors['2g2i'],
|
||||
});
|
||||
|
||||
domains(_2g2i, _2g2i_poly_repr, [
|
||||
[Domains.SH2, DomainColors['SH2_BCR'], 'SH2'],
|
||||
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
|
||||
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (active)', { label_size: 3 }],
|
||||
], { label_size: 3 });
|
||||
|
||||
drawInteractions(_2g2i, [
|
||||
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_2g2i, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [49.01, 78.47, 38.92],
|
||||
target: [15.59, 54.81, 12.37],
|
||||
up: [0.61, 0.03, -0.79],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'ATP Binding and Unstoppable Signaling [2/2]',
|
||||
description: `
|
||||
### ATP Binding and Unstoppable Signaling
|
||||
|
||||
Note the location of the activation loop (in red) which sits in its active conformation.
|
||||
|
||||
In normal cells, kinases like ABL are tightly regulated — ATP binding and activation only occur when needed. But in BCR-ABL, this regulation is lost.
|
||||
ATP binds freely, phosphorylation proceeds unchecked, and the signaling pathways that drive leukemia remain constantly switched on.
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps.find((s: any) => s.key === 'unstoppable-signaling')?.state()!;
|
||||
},
|
||||
camera: {
|
||||
position: [98.66, 82.23, 14.15],
|
||||
target: [12.31, 54.23, 18.79],
|
||||
up: [0.06, -0.35, -0.93],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Imatinib: The Drug That Changed Everything [1/2]',
|
||||
key: 'imatinib',
|
||||
description: `
|
||||
### Imatinib: The Drug That Changed Everything
|
||||
|
||||
For years, chronic myeloid leukemia (CML) was a death sentence. Then came Imatinib (Gleevec) — a small molecule designed to fit into the ATP-binding pocket of BCR-ABL and lock the kinase in an inactive conformation. It was the first targeted cancer therapy of its kind.
|
||||
|
||||
Take a look at the Imatinib-bound structure ([PDB ID 1IEP](${wwPDBLink('1iep')})), and you'll notice a key difference — this time, the kinase is frozen in its inactive form. The drug (shown in colour) nestles deep in the ATP-binding site and blocks ATP from binding.
|
||||
|
||||
Imatinib forms specific interactions with several important residues:
|
||||
- A hydrogen bond with Thr315, the gatekeeper residue, which plays a major role in drug sensitivity and resistance.
|
||||
- Asp381, part of the DFG motif, which helps coordinate catalytic magnesium ions and position the phosphate for transfer.
|
||||
- Glu286, located in the αC-helix, normally forms a salt bridge with Lys271 in the active conformation — but here, it's flipped away.
|
||||
- Ile360 and His361, part of the activation loop, help stabilize the inactive conformation that Imatinib prefers.
|
||||
|
||||
Together, these interactions stabilize the inactive kinase, shutting down its activity and halting the signaling cascade that drives leukemia.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
drawInteractions(_1iep, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'Thr315',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
], { color: Colors['binding-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [40.32, 68.65, 13.5],
|
||||
target: [16, 53.82, 14.88],
|
||||
up: [0.26, -0.5, -0.83],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Imatinib: The Drug That Changed Everything [2/2]',
|
||||
description: `
|
||||
### Imatinib: The Drug That Changed Everything
|
||||
|
||||
Notice how the P-loop, which normally cradles ATP in the active state, which was not visible in the active state, has shifted into a stabilised closed and collapsed conformation.
|
||||
At the same time, the activation loop (in green), which needs to be extended and open for the kinase to catalyse phosphorylation, is now flipped into a closed, inactive position.
|
||||
|
||||
Imatinib doesn't just block ATP from binding — it locks BCR-ABL into an inactive conformation, one where the active site is misaligned and the kinase simply can't function.
|
||||
|
||||
By switching between the ADP-bound active structure and the Imatinib-bound inactive structure, you can clearly see the conformational changes. The shift is dramatic and decisive: the enzyme goes from a catalytically ready state to one that is completely switched off.
|
||||
|
||||
The change is decisive: BCR-ABL is finally silenced.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
domains(_1iep, _1iep_poly_repr, [
|
||||
[Domains.P_loop, DomainColors['P_loop'], 'P Loop'],
|
||||
[Domains.Activation_loop, DomainColors['Activation_loop'], 'Activation Loop (inactive)', { label_size: 3 }],
|
||||
], { label_size: 3 });
|
||||
|
||||
drawInteractions(_1iep, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'G', label_atom_id: 'N21' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 315, auth_atom_id: 'OG1' }, { label_asym_id: 'G', label_atom_id: 'N13' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N3' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'G', label_atom_id: 'N51' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'G', label_atom_id: 'O29' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'Thr315',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
bindingSite(_1iep, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
], { color: Colors['binding-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [91.47, 73.63, 20.78],
|
||||
target: [12.53, 54.2, 19.09],
|
||||
up: [0.04, -0.07, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Resistance Strikes: The T315I Mutation [1/2]',
|
||||
key: 'mutation',
|
||||
description: `
|
||||
### Resistance Strikes: The T315I Mutation
|
||||
|
||||
For a while, it seemed like leukemia had been beaten. But then, in some patients, the cancer returned. The culprit?
|
||||
What was once a threonine (Thr) is now an isoleucine (Ile), a single mutation [T315I](https://doi.org/10.1016/j.ccr.2011.03.003), shown on [PDB ID 3IK3](${wwPDBLink('3ik3')}) in orange.
|
||||
|
||||
Forming a hydrogen bond with Imatinib, Thr315 was a crucial contact point. With bulkier and non-polar isoleucine in its place, the contact is lost and the drug won't bind.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1iep = structure(builder, '1iep');
|
||||
const [, _1iep_poly_repr] = polymer(_1iep, { color: Colors['1iep'] });
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { label_asym_id: 'G' },
|
||||
surface: true,
|
||||
label: 'Imatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['1iep'],
|
||||
});
|
||||
|
||||
ligand(_1iep, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
carbon_color: Colors['1iep'],
|
||||
opacity: 0.51,
|
||||
});
|
||||
|
||||
const _3ik3 = structure(builder, '3ik3');
|
||||
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
|
||||
|
||||
ligand(_3ik3, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [13.69, 72.8, 4.44],
|
||||
target: [13.02, 54.12, 9.71],
|
||||
up: [0.39, -0.26, -0.88],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Resistance Strikes: The T315I Mutation [2/2]',
|
||||
description: `
|
||||
### Resistance Strikes: The T315I Mutation
|
||||
|
||||
This mutation prevents Imatinib binding but still allows ATP (here represented by the ADP) to nestle into the active site.
|
||||
The result? Resistance. BCR-ABL is active again, and the leukemia returns, this time untouchable by Imatinib.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _2g2i = structure(builder, '2g2i');
|
||||
const [, _2g2i_poly_repr] = polymer(_2g2i, { color: Colors['2g2i'] });
|
||||
|
||||
ligand(_2g2i, {
|
||||
selector: { label_asym_id: 'E' },
|
||||
surface: true,
|
||||
label: 'ADP',
|
||||
label_size: 2,
|
||||
label_color: Colors['2g2i'],
|
||||
});
|
||||
|
||||
drawInteractions(_2g2i, [
|
||||
['Salt Bridge', { auth_asym_id: 'A', auth_seq_id: 271, auth_atom_id: 'NZ' }, { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_2g2i, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 271 }, 'Lys271'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
const _3ik3 = structure(builder, '3ik3');
|
||||
const [, _3ik3_poly_repr] = polymer(_3ik3, { color: Colors['3ik3'] });
|
||||
|
||||
ligand(_3ik3, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [-3.29, 89.29, 2.7],
|
||||
target: [16.64, 55.48, 15.94],
|
||||
up: [0.24, -0.23, -0.94],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'Fighting Back: Ponatinib and the Future of Kinase Inhibitors',
|
||||
key: 'ponatinib',
|
||||
description: `
|
||||
### Fighting Back: Ponatinib and the Future of Kinase Inhibitors
|
||||
|
||||
The battle didn't end there. Scientists knew they needed a new inhibitor—one that could work even against T315I. Enter Ponatinib (shown in [PDB ID 3OXZ](${wwPDBLink('3oxz')})), a next-generation
|
||||
drug designed to bypass this resistance. Viewing the Ponatinib-bound structure, you'll see how it differs from Imatinib. Instead of being blocked by T315I,
|
||||
Ponatinib has a flexible triple-bond linker, allowing it to slip into the binding site without clashing with the mutation.
|
||||
|
||||
Look closely at the interactions—Ponatinib forms new hydrophobic contacts that compensate for the loss of the Thr315 interaction. This structure tells a story of rational drug design: scientists
|
||||
used everything they learned about BCR-ABL's structure to engineer a molecule that could fit where others failed.
|
||||
|
||||
But the story isn't over. New mutations continue to arise, and leukemia is still finding ways to outmaneuver our drugs. The future may lie in allosteric
|
||||
inhibitors that bind outside the ATP pocket, or even in protein degradation strategies that eliminate BCR-ABL entirely. Whatever the next breakthrough is,
|
||||
it will start here—with a deep understanding of structure and function, and the power of visualization to reveal the molecular battles happening
|
||||
inside every cancer cell.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _3oxz = structure(builder, '3oxz');
|
||||
const [, _3oxz_poly_repr] = polymer(_3oxz, { color: Colors['3oxz'] });
|
||||
|
||||
ligand(_3oxz, {
|
||||
selector: { label_asym_id: 'B' },
|
||||
surface: true,
|
||||
label: 'Ponatinib',
|
||||
label_size: 2,
|
||||
label_color: Colors['3oxz'],
|
||||
});
|
||||
|
||||
ligand(_3oxz, {
|
||||
selector: { auth_asym_id: 'A', auth_seq_id: 315 },
|
||||
label: 'T315I',
|
||||
label_size: 2,
|
||||
carbon_color: 'red',
|
||||
label_color: 'red',
|
||||
});
|
||||
|
||||
drawInteractions(_3oxz, [
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 360, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 361, auth_atom_id: 'O' }, { label_asym_id: 'B', label_atom_id: 'N4' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 286, auth_atom_id: 'OE2' }, { label_asym_id: 'B', label_atom_id: 'N2' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 381, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'O1' }, { skipResidue: true }],
|
||||
['H-bond', { auth_asym_id: 'A', auth_seq_id: 318, auth_atom_id: 'N' }, { label_asym_id: 'B', label_atom_id: 'N1' }, { skipResidue: true }],
|
||||
]);
|
||||
|
||||
bindingSite(_3oxz, [
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 286 }, 'Glu286'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 318 }, 'Met318'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 360 }, 'Ile360'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 361 }, 'His361'],
|
||||
[{ auth_asym_id: 'A', auth_seq_id: 381 }, 'Asp381'],
|
||||
], { color: Colors['active-site'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [61.15, 66.58, 19.72],
|
||||
target: [9.61, 50.49, 14.08],
|
||||
up: [0.15, -0.15, -0.98],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The End',
|
||||
key: 'end',
|
||||
description: `
|
||||
### The End
|
||||
|
||||
That's all folks! We hope you enjoyed this interactive journey through the structural biology of BCR-ABL.
|
||||
|
||||
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
|
||||
|
||||
Read more [here](https://pmc.ncbi.nlm.nih.gov/articles/PMC3513788/).
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps[0].state();
|
||||
},
|
||||
camera: {
|
||||
position: [103.72, 69.35, 20.52],
|
||||
target: [0.36, 55.32, 21.8],
|
||||
up: [-0.01, 0.01, -1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
type Interaction = [label: string, polymer: PrimitivePositionT, ligand: PrimitivePositionT, options?: { skipResidue?: boolean }]
|
||||
|
||||
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
|
||||
const primitives = structure.primitives();
|
||||
|
||||
const interactingResidues: ComponentExpressionT[] = [];
|
||||
const addedResidues = new Set<string>();
|
||||
|
||||
for (const [tooltip, a, b, options] of interactions) {
|
||||
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
|
||||
|
||||
if (options?.skipResidue) continue;
|
||||
|
||||
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
|
||||
for (const _e of expressions) {
|
||||
const e = { ..._e };
|
||||
delete e.auth_atom_id;
|
||||
delete e.label_atom_id;
|
||||
|
||||
const key = JSON.stringify(e);
|
||||
if (addedResidues.has(key)) continue;
|
||||
interactingResidues.push(e);
|
||||
addedResidues.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (interactingResidues.length === 0) return;
|
||||
|
||||
structure
|
||||
.component({ selector: interactingResidues })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
|
||||
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
|
||||
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
|
||||
return structure.transform({ rotation, translation });
|
||||
}
|
||||
|
||||
function wwPDBLink(id: string) {
|
||||
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
|
||||
}
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
let ret = builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
|
||||
if (id in Superpositions) {
|
||||
ret = transform(ret, id as any);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function domains(structure: MVSStructure, reprensentation: Representation, domains: [selector: ComponentExpressionT, color: ColorT, label?: string, options?: { label_size?: number }][], options?: { label_size?: number }) {
|
||||
const hasLabels = domains.some(d => !!d[2]);
|
||||
const primitives = hasLabels ? structure.primitives() : undefined;
|
||||
|
||||
for (const [selector, color, label, opts] of domains) {
|
||||
reprensentation.color({ selector, color });
|
||||
if (label) primitives!.label({ position: selector, text: label, label_color: color, label_size: opts?.label_size ?? options?.label_size ?? 1.5 });
|
||||
}
|
||||
}
|
||||
|
||||
function polymer(structure: MVSStructure, options: { color: ColorT }) {
|
||||
const component = structure.component({ selector: { label_asym_id: 'A' } });
|
||||
const reprensentation = component.representation({ type: 'cartoon' });
|
||||
reprensentation.color({ color: options.color });
|
||||
return [component, reprensentation] as const;
|
||||
}
|
||||
|
||||
function ligand(structure: MVSStructure, options: {
|
||||
selector: ComponentExpressionT | ComponentExpressionT[],
|
||||
label?: string,
|
||||
surface?: boolean,
|
||||
carbon_color?: ColorT,
|
||||
uniform_color?: ColorT,
|
||||
label_color?: ColorT,
|
||||
label_size?: number,
|
||||
opacity?: number,
|
||||
}) {
|
||||
const comp = structure.component({ selector: options.selector });
|
||||
const coloring = options.uniform_color
|
||||
? { color: options.uniform_color }
|
||||
: {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: {} } }
|
||||
}
|
||||
};
|
||||
|
||||
if (options.surface) comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
|
||||
const repr = comp.representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
if (options.opacity) repr.opacity({ opacity: options.opacity });
|
||||
|
||||
const label_color: ColorT = options?.label_color ?? options.uniform_color ?? options.carbon_color ?? '#5B53A4';
|
||||
if (options.label) {
|
||||
structure.primitives().label({
|
||||
position: Array.isArray(options.selector) ? { expressions: options.selector } : options.selector,
|
||||
text: options.label,
|
||||
label_color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
|
||||
color?: ColorT,
|
||||
label_size?: number,
|
||||
}) {
|
||||
const color: ColorT = options.color ?? '#5B53A4';
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
|
||||
}
|
||||
};
|
||||
|
||||
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
|
||||
const primitives = structure.primitives();
|
||||
for (const [selector, label] of residues) {
|
||||
primitives.label({
|
||||
position: selector,
|
||||
text: label,
|
||||
label_color: color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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: 5000,
|
||||
transition_duration_ms: s.transition_duration_ms ?? 1500,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'The Structural Story of BCR-ABL: A Kinase Out of Control',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
541
src/examples/mvs-stories/stories/tbp.ts
Normal file
541
src/examples/mvs-stories/stories/tbp.ts
Normal file
@@ -0,0 +1,541 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
*/
|
||||
|
||||
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,
|
||||
ComponentExpressionT,
|
||||
isPrimitiveComponentExpressions,
|
||||
PrimitivePositionT
|
||||
} from '../../../extensions/mvs/tree/mvs/param-types';
|
||||
import { Mat3, Mat4, Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { decodeColor } from '../../../extensions/mvs/helpers/utils';
|
||||
|
||||
const Colors = {
|
||||
'1vok': '#4577B2' as ColorT,
|
||||
'1cdw': '#BC536D' as ColorT,
|
||||
'1cdw-2': '#c5a3af' as ColorT,
|
||||
'1vtl': '#B9E3A0' as ColorT,
|
||||
'7enc': '#0072B2' as ColorT,
|
||||
'7enc-2': '#D55E00' as ColorT,
|
||||
'7enc-3': '#009E73' as ColorT,
|
||||
'7enc-4': '#56B4E9' as ColorT,
|
||||
};
|
||||
|
||||
// Obtained using https://www.rcsb.org/alignment
|
||||
const Superpositions = {
|
||||
'1cdw': [-0.4665815186, 0.6063873444, -0.6438913535, 0, -0.581544075, -0.7588303199, -0.2932286385, 0, -0.6664144171, 0.2376361381, 0.7066971703, 0, 135.0863694935, 105.5007997009, 153.6890178993, 1] as unknown as Mat4,
|
||||
'1vtl': [-0.4769460004, 0.7214347188, -0.5020502557, 0, -0.297882932, 0.4047204695, 0.8645617968, 0, 0.8269149119, 0.5619014932, 0.0218732801, 0, 65.6043682658, -3.7328402905, -16.8650755387, 1] as unknown as Mat4,
|
||||
'7enc': [0.8975055044, -0.4316347566, -0.0904174009, 0, 0.247274877, 0.3227849997, 0.9136000105, 0, -0.3651561375, -0.8423189899, 0.3964337454, 0, -189.7572972798, 304.0841220076, -411.5005782853, 1] as unknown as Mat4,
|
||||
};
|
||||
|
||||
const Steps = [
|
||||
{
|
||||
header: 'TATA-Binding Protein',
|
||||
key: 'intro',
|
||||
description: `
|
||||
### TATA-Binding Protein Tells RNA Polymerase Where To Get Started on a Gene
|
||||
|
||||
Specialized DNA sequences next to genes, called promoters, define the proper start site and direction for transcription.
|
||||
Promoters vary in sequence and location from organism to organism.
|
||||
In eukaryotic cells, a complex promoter system that uses dozens of different proteins ensures that the proper RNA polymerase is targeted to each gene.
|
||||
The TATA-binding protein (TBP) is the central element of this system, participating in transcription by all three RNA polymerases.
|
||||
|
||||
The crystal structure of TBP from *Arabidopsis thaliana* in its apo form is shown ([PDB ID 1VOK](${wwPDBLink('1vok')})).
|
||||
The structure reveals a highly symmetric DNA-binding fold composed of two topologically identical domains derived from the two direct repeats in the phylogenetically conserved sequence.
|
||||
The domains are related by an approximate 2-fold axis, and consist of a five-stranded, antiparallel beta sheet and two alpha helices.
|
||||
It is thought that an ancient gene duplication created this protein by combining two copies of the same gene.
|
||||
|
||||
The intramolecular symmetry generates a saddle-shaped structure dominated by a curved antiparallel beta sheet, which forms the saddle's concave face and interacts with the DNA.
|
||||
The convex face of the saddle would interact with other proteins during transcription initiation.
|
||||
`,
|
||||
state: (): Root => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1vok = structure(builder, '1vok');
|
||||
const _1vok_comp = _1vok.component({ selector: { label_asym_id: 'A' } });
|
||||
_1vok_comp.representation({ type: 'cartoon' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'sequence-id',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'sequence-id', params: {} } },
|
||||
}
|
||||
});
|
||||
_1vok.primitives().label({
|
||||
position: { label_asym_id: 'A', label_seq_id: 88, label_atom_id: 'OD2' },
|
||||
text: 'TATA-Binding Protein',
|
||||
label_size: 5,
|
||||
label_color: Colors['1vok']
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [155.18, 118.49, 49.18],
|
||||
target: [74.81, 66.8, 30.7],
|
||||
up: [-0.55, 0.83, 0.1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Highly Conserved in Eukaryotes [1/2]',
|
||||
key: 'highly-conserved-1',
|
||||
description: `
|
||||
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
|
||||
|
||||
TBP has a phylogenetically conserved, 180 amino-acid carboxy-terminal domain (ranging from 38 to 93% identity among eukaryotes and archaebacteria), containing two structural repeats flanking a highly basic segment known as the basic repeat.
|
||||
The C-terminal or core portion of the protein binds the TATA consensus sequence with high affinity, interacting with the minor groove and promoting DNA bending.
|
||||
Structural superposition of TBP bound to DNA in human ([PDB ID 1CDW](${wwPDBLink('1cdw')})) and *A. thaliana* ([PDB ID 1VTL](${wwPDBLink('1vtl')})) shows that their sequences are 83% identical and the RMSD between the structures is 0.43 Å.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 })[0].label({ text: 'DNA' });
|
||||
// uses a range to 'adjust' font size of label
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', beg_label_seq_id: 160, end_label_seq_id: 177 }, text: 'TBP (H. sapiens)' });
|
||||
|
||||
const _1vtl = structure(builder, '1vtl');
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
|
||||
// uses a range to 'adjust' font size of label
|
||||
label(_1vtl, { selector: { label_asym_id: 'E', beg_label_seq_id: 75, end_label_seq_id: 92 }, text: 'TBP (A. thaliana)' });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Minor Groove [2/2]',
|
||||
key: 'highly-conserved-2',
|
||||
description: `
|
||||
### TATA-Binding Protein Is Highly Conserved in Eukaryotes
|
||||
|
||||
Eukaryotic protein-coding genes transcribed by RNA polymerase II (pol II) have a characteristic sequence of nucleotides, termed the TATA box, in front of the start site of transcription.
|
||||
The typical sequence is something like T-A-T-A-a/t-A-a/t, where a/t refers to positions that can be either A or T.
|
||||
TBP recognizes this TATA sequence and binds to it, creating a landmark that directs pol II to the transcription start site.
|
||||
The structures of *A. thaliana* and human core TBP-TATA element co-crystal structures demonstrate a common induced-fit mechanism of protein-DNA recognition involving subtle conformation changes in the protein and an unprecedented DNA distortion.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'protein' });
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: 'nucleic', opacity: 0.5 });
|
||||
// select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'A', beg_label_seq_id: 6, end_label_seq_id: 11 } })[0].label({ text: 'TATA box' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 5 }, text: 'T' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 6 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 7 }, text: 'T' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 8 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 9 }, text: 'A' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'A', label_seq_id: 10 }, text: 'A' });
|
||||
|
||||
const _1vtl = structure(builder, '1vtl');
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'E' } });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'A' }, opacity: 0.5 });
|
||||
select(_1vtl, { color: Colors['1vtl'], selector: { label_asym_id: 'B' }, opacity: 0.5 });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [108.48, 92.12, 4.1],
|
||||
target: [80.16, 56.15, 28.96],
|
||||
up: [-0.71, 0.68, 0.17],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Binding to TATA Box [1/5]',
|
||||
key: 'tata-box-overview',
|
||||
description: `
|
||||
### TATA-Binding Protein Binds to the Minor Groove of the TATA Box
|
||||
|
||||
When the first structures of TBP-DNA complexes were determined, researchers discovered that TBP is not gentle when it binds to DNA.
|
||||
Instead, it grabs the TATA sequence, bends and unwinds it to open up the minor groove, and kinks it sharply in two places (*e.g.*, [PDB ID 1VTL](${wwPDBLink('1vtl')})) and ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
|
||||
|
||||
Interactions with the minor groove can be divided into different classes as seen in PDB ([PDB ID 1CDW](${wwPDBLink('1cdw')})).
|
||||
The combination of interactions allows TATA-binding protein to recognize the proper DNA sequence.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Arginine [2/5]',
|
||||
key: 'tata-box-1',
|
||||
description: `
|
||||
### Arginine Residues
|
||||
|
||||
A string of arginine amino acids interact with the phosphate groups of the DNA and glues the protein to the DNA: Arg192, Arg199, Arg204, and Arg290.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 38 }, text: 'Arg192' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 6, label_atom_id: `O3'` }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 38, label_atom_id: 'NH1' }, { label_asym_id: 'B', label_seq_id: 7, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 45 }, text: 'Arg199' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NE' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 45, label_atom_id: 'NH2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 136 }, text: 'Arg290' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 136, label_atom_id: 'NH1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'OP1' }],
|
||||
]);
|
||||
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 50 }, text: 'Arg204' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH1' }],
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'OP1' }, { label_asym_id: 'C', label_seq_id: 50, label_atom_id: 'NH2' }],
|
||||
]);
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [113.87, 71.89, 26.29],
|
||||
target: [77.29, 61.61, 18.73],
|
||||
up: [-0.28, 0.96, 0.04],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Phenylalanine [3/5]',
|
||||
key: 'tata-box-2',
|
||||
description: `
|
||||
### Phenylalanine-Induced Kinks
|
||||
|
||||
At either end of the TATA element there are two pairs of phenylalanine side chains partially inserted between adjacent base pairs, producing the two dramatic kinks (Phe193, Phe210, Phe284, and Phe301). Between the two kinks the DNA is partially unwound.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
bindingSite(_1cdw, [
|
||||
[{ label_asym_id: 'C', label_seq_id: 39 }, 'Phe193'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 56 }, 'Phe210'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 130 }, 'Phe284'],
|
||||
[{ label_asym_id: 'C', label_seq_id: 147 }, 'Phe301'],
|
||||
], { color: Colors['1cdw'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [111.01, 69.92, -3.14],
|
||||
target: [85.52, 57.53, 19.15],
|
||||
up: [-0.51, 0.85, -0.11],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: H-Bonds in Minor Groove [4/5]',
|
||||
key: 'tata-box-3',
|
||||
description: `
|
||||
### Hydrogen Bonds in the Minor Groove
|
||||
|
||||
Polar side chains make minor groove hydrogen bonds with acceptors of base pairs centred about the approximate 2-fold symmetry axis (Asn163, Asn253, and Thr309).
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
// Asn: 9, 99, 155
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 9 }, text: 'Asn163' });
|
||||
label(_1cdw, { selector: { label_asym_id: 'C', label_seq_id: 155 }, text: 'Thr309' });
|
||||
drawInteractions(_1cdw, [
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }, { label_asym_id: 'B', label_seq_id: 8, label_atom_id: 'O2' }],
|
||||
['H-bond', { label_asym_id: 'B', label_seq_id: 9, label_atom_id: 'O2' }, { label_asym_id: 'C', label_seq_id: 9, label_atom_id: 'ND2' }],
|
||||
['H-bond', { label_asym_id: 'C', label_seq_id: 155, label_atom_id: 'OG1' }, { label_asym_id: 'A', label_seq_id: 8, label_atom_id: 'N3' }],
|
||||
]);
|
||||
bindingSite(_1cdw, [
|
||||
[{ label_asym_id: 'C', label_seq_id: 99 }, 'Asn253'],
|
||||
], { color: 'gray', label_color: Colors['1cdw'] });
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [69.71, 56.65, 17.62],
|
||||
target: [75.68, 63.48, 32.29],
|
||||
up: [-0.9, 0.39, 0.18],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP: Non-Polar Interactions [5/5]',
|
||||
key: 'tata-box-4',
|
||||
description: `
|
||||
### Hydrophobic and van der Waals Interactions
|
||||
|
||||
Several residues projecting from the concave surface of TBP make hydrophobic or van der Waals side-chain/base contacts (<4 Å between non-hydrogen atoms) with the TATA element.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _1cdw = structure(builder, '1cdw');
|
||||
select(_1cdw, { color: Colors['1cdw'], selector: { label_asym_id: 'C' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'A' } });
|
||||
select(_1cdw, { color: Colors['1cdw-2'], selector: { label_asym_id: 'B' } });
|
||||
|
||||
surface(_1cdw, {
|
||||
selector: { label_asym_id: 'A' },
|
||||
carbon_color: Colors['1cdw-2'],
|
||||
});
|
||||
surface(_1cdw, {
|
||||
selector: { label_asym_id: 'B' },
|
||||
carbon_color: Colors['1cdw-2'],
|
||||
});
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'TBP and Transcription Pre-Initiation Complex',
|
||||
key: 'pre-init-complex',
|
||||
description: `
|
||||
### TATA-Binding Protein and the Transcription Pre-Initiation Complex
|
||||
|
||||
The structure shown here, from [PDB ID 7ENC](${wwPDBLink('7enc')}), is that of a pol II “pre-initiation complex” (PIC) poised to start transcription.
|
||||
This complex includes eukaryotic Mediator and pol II, along with a collection of general transcription factors that perform the necessary tasks of recognizing the transcription start site of genes, separating the two strands of the DNA double helix, and facilitating transcription initiation by RNA polymerase II.
|
||||
|
||||
TBP starts the process of assembling the PIC and works with TBP-associated factors (TAFs) as part of a large multi-component transcription factor, TFIID, that belongs to the collection of general transcription factors that cradle pol II with in the PIC.
|
||||
After TBP binds to the promoter, it recruits additional transcription factors.
|
||||
TFIIA and TFIIB interact with surrounding regions of the DNA and, along with TFIIF, assist with positioning of RNA polymerase II at the transcription start site.
|
||||
TFIIE, TFIIH, and other TFIID subunits bring additional functionality to the complex.
|
||||
In particular, part of TFIIH is a translocase that separates the two strands of DNA in preparation for transcription, and the CAK module of TFIIH adds phosphate groups to the long tail of RNA polymerase II, sending the signal that it is time to get started with mRNA synthesis.
|
||||
|
||||
TBP is a critical player that gets the transcription process started and is central to the pre-initiation complex.
|
||||
Lying at the very heart of this complex macromolecular machine, it is highly conserved among eukaryotes and *archaea*.
|
||||
`,
|
||||
state: () => {
|
||||
const builder = createMVSBuilder();
|
||||
|
||||
const _7enc = structure(builder, '7enc');
|
||||
select(_7enc, { color: Colors['7enc'], selector: { label_asym_id: 'CB' } })[0].label({ text: 'TBP' });
|
||||
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'GB' } });
|
||||
select(_7enc, { color: Colors['7enc-2'], selector: { label_asym_id: 'HB' } });
|
||||
select(_7enc, { color: Colors['7enc-3'], opacity: 0.5 });
|
||||
|
||||
_7enc.primitives()
|
||||
.label({ position: { label_entity_id: '57' }, text: 'pol II', label_size: 20, label_color: Colors['7enc'] })
|
||||
.label({ position: { label_entity_id: '53' }, text: 'DNA', label_size: 20, label_color: Colors['7enc-2'] })
|
||||
.label({ position: { label_entity_id: '21' }, text: 'mediator', label_size: 20, label_color: Colors['7enc-3'] })
|
||||
.label({ position: { label_entity_id: '42' }, text: 'TBP-associated factors (TAFs)', label_size: 20, label_color: Colors['7enc-4'] })
|
||||
.label({ position: [100, 150, 0], text: 'PIC', label_size: 30, label_color: Colors['1vok'] })
|
||||
;
|
||||
|
||||
return builder;
|
||||
},
|
||||
camera: {
|
||||
position: [122.15, 104.37, 72.23],
|
||||
target: [77.48, 59.61, 30.36],
|
||||
up: [-0.73, 0.68, 0.06],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}, {
|
||||
header: 'The End',
|
||||
key: 'end',
|
||||
description: `
|
||||
### The End
|
||||
|
||||
That's all folks! We hope you enjoyed this interactive journey through the structural biology of the TATA-binding protein.
|
||||
|
||||
The next time you look at a macromolecular structure, remember: each atom tells a story, and each discovery shapes the future of medicine.
|
||||
|
||||
Read more in the relevant publications on PDB IDs [1VOK](https://doi.org/10.1038/nsb0994-621), [1CDW](https://doi.org/10.1073/pnas.93.10.4862), [1VTL](https://doi.org/10.1038/365520a0), and [7ENC](https://doi.org/10.1126/science.abg0635) as well as the Molecule of the Month articles on the [TATA-binding protein](https://pdb101.rcsb.org/motm/67) and [Mediator](https://pdb101.rcsb.org/motm/289).
|
||||
`,
|
||||
state: (): Root => {
|
||||
return Steps[Steps.length - 2].state();
|
||||
},
|
||||
camera: {
|
||||
position: [461.95, 218.66, 140.67],
|
||||
target: [87.56, -22.14, 54.59],
|
||||
up: [-0.55, 0.83, 0.1],
|
||||
} satisfies MVSNodeParams<'camera'>,
|
||||
}
|
||||
];
|
||||
|
||||
type Interaction = [label: string, res1: PrimitivePositionT, res2: PrimitivePositionT, options?: { skipResidue?: boolean }]
|
||||
|
||||
function drawInteractions(structure: MVSStructure, interactions: Interaction[]) {
|
||||
const primitives = structure.primitives();
|
||||
|
||||
const interactingResidues: ComponentExpressionT[] = [];
|
||||
const addedResidues = new Set<string>();
|
||||
|
||||
function drawResidue(a: PrimitivePositionT) {
|
||||
const expressions = isPrimitiveComponentExpressions(a) ? a.expressions! : [a as ComponentExpressionT];
|
||||
for (const _e of expressions) {
|
||||
const e = { ..._e };
|
||||
delete e.auth_atom_id;
|
||||
delete e.label_atom_id;
|
||||
|
||||
const key = JSON.stringify(e);
|
||||
if (addedResidues.has(key)) continue;
|
||||
interactingResidues.push(e);
|
||||
addedResidues.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [tooltip, a, b, options] of interactions) {
|
||||
primitives.tube({ start: a, end: b, color: '#4289B5', tooltip, radius: 0.1, dash_length: 0.1 });
|
||||
|
||||
if (options?.skipResidue) continue;
|
||||
|
||||
drawResidue(a);
|
||||
drawResidue(b);
|
||||
}
|
||||
|
||||
if (interactingResidues.length === 0) return;
|
||||
|
||||
structure
|
||||
.component({ selector: interactingResidues })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'element-symbol', params: {} } },
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function transform(structure: MVSStructure, id: keyof typeof Superpositions) {
|
||||
const rotation = Mat3.fromMat4(Mat3.zero(), Superpositions[id]);
|
||||
const translation = Mat4.getTranslation(Vec3.zero(), Superpositions[id]) as any;
|
||||
return structure.transform({ rotation, translation });
|
||||
}
|
||||
|
||||
function wwPDBLink(id: string) {
|
||||
return `https://doi.org/10.2210/pdb${id.toLowerCase()}/pdb`;
|
||||
}
|
||||
|
||||
function structure(builder: Root, id: string): MVSStructure {
|
||||
let ret = builder
|
||||
.download({ url: pdbUrl(id) })
|
||||
.parse({ format: 'bcif' })
|
||||
.modelStructure();
|
||||
|
||||
if (id in Superpositions) {
|
||||
ret = transform(ret, id as any);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
function select(structure: MVSStructure, { color, opacity = 1.0, selector = 'polymer' }: { color: ColorT, opacity?: number, selector?: Partial<MVSNodeParams<'component'>>['selector'] }) {
|
||||
const component = structure.component({ selector });
|
||||
const representation = component.representation({ type: 'cartoon' });
|
||||
representation.color({ color }).opacity({ opacity });
|
||||
return [component, representation] as const;
|
||||
}
|
||||
|
||||
function label(structure: MVSStructure, { selector, text }: { selector: Partial<MVSNodeParams<'component'>>['selector'], text: string }) {
|
||||
structure.component({ selector })
|
||||
.label({ text });
|
||||
}
|
||||
|
||||
function surface(structure: MVSStructure, options: {
|
||||
selector: ComponentExpressionT | ComponentExpressionT[],
|
||||
carbon_color?: ColorT,
|
||||
}) {
|
||||
const comp = structure.component({ selector: options.selector });
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: options?.carbon_color ? { name: 'uniform', params: { value: decodeColor(options?.carbon_color) } } : { name: 'element-symbol', params: { } } }
|
||||
}
|
||||
};
|
||||
comp.representation({ type: 'surface' }).color(coloring).opacity({ opacity: 0.33 });
|
||||
|
||||
return comp;
|
||||
}
|
||||
|
||||
function bindingSite(structure: MVSStructure, residues: [selector: ComponentExpressionT, label: string][], options: {
|
||||
color?: ColorT,
|
||||
label_size?: number,
|
||||
label_color?: ColorT,
|
||||
}) {
|
||||
const color: ColorT = options.color ?? '#5B53A4';
|
||||
const coloring = {
|
||||
custom: {
|
||||
molstar_color_theme_name: 'element-symbol',
|
||||
molstar_color_theme_params: { carbonColor: { name: 'uniform', params: { value: decodeColor(color) } } }
|
||||
}
|
||||
};
|
||||
|
||||
structure.component({ selector: residues.map(r => r[0]) }).representation({ type: 'ball_and_stick' }).color(coloring);
|
||||
|
||||
const primitives = structure.primitives();
|
||||
for (const [selector, label] of residues) {
|
||||
primitives.label({
|
||||
position: selector,
|
||||
text: label,
|
||||
label_color: options?.label_color ?? color,
|
||||
label_size: options?.label_size ?? 1.5
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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: 5000,
|
||||
transition_duration_ms: 1500,
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
kind: 'multiple',
|
||||
snapshots,
|
||||
metadata: {
|
||||
title: 'The Structural Story of TATA-Binding Protein and its Role in Transcription Initiation',
|
||||
version: '1.0',
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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>
|
||||
*/
|
||||
@@ -15,11 +15,11 @@ import { PluginContext } from '../../mol-plugin/context';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Theme } from '../../mol-theme/theme';
|
||||
import { VolumeRepresentation3DHelpers } from '../../mol-plugin-state/transforms/representation';
|
||||
import { AlphaOrbital, Basis, CubeGrid, 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),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Location } from '../../mol-model/location';
|
||||
import { ScaleLegend, TableLegend } from '../../mol-util/legend';
|
||||
import { getPalette, getPaletteParams } from '../../mol-util/color/palette';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { ColorThemeCategory } from '../../mol-theme/color/categories';
|
||||
|
||||
const DefaultColor = Color(0xCCCCCC);
|
||||
|
||||
@@ -102,7 +103,7 @@ export function AssemblySymmetryClusterColorTheme(ctx: ThemeDataContext, props:
|
||||
export const AssemblySymmetryClusterColorThemeProvider: ColorTheme.Provider<AssemblySymmetryClusterColorThemeParams, AssemblySymmetryData.Tag.Cluster> = {
|
||||
name: AssemblySymmetryData.Tag.Cluster,
|
||||
label: 'Assembly Symmetry Cluster',
|
||||
category: ColorTheme.Category.Symmetry,
|
||||
category: ColorThemeCategory.Symmetry,
|
||||
factory: AssemblySymmetryClusterColorTheme,
|
||||
getParams: getAssemblySymmetryClusterColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(AssemblySymmetryClusterColorThemeParams),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 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 Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Structure, Model, StructureSelection, QueryContext } from '../../mol-model/structure';
|
||||
import { Database as _Database, Column } from '../../mol-data/db';
|
||||
import { Column } from '../../mol-data/db';
|
||||
import { GraphQLClient } from '../../mol-util/graphql-client';
|
||||
import { CustomProperty } from '../../mol-model-props/common/custom-property';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
@@ -74,11 +74,8 @@ export namespace AssemblySymmetryData {
|
||||
export const DefaultServerUrl = 'https://data.rcsb.org/graphql'; // Alternative: 'https://www.ebi.ac.uk/pdbe/aggregated-api/pdb/symmetry' (if serverType is 'pdbe')
|
||||
|
||||
export function isApplicable(structure?: Structure): boolean {
|
||||
return (
|
||||
!!structure && structure.models.length === 1 &&
|
||||
Model.hasPdbId(structure.models[0]) &&
|
||||
isBiologicalAssembly(structure)
|
||||
);
|
||||
if (!structure || structure.models.length !== 1 || !Model.hasPdbId(structure.models[0])) return false;
|
||||
return isBiologicalAssembly(structure) || Model.isIntegrative(structure.models[0]);
|
||||
}
|
||||
|
||||
export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
|
||||
@@ -91,7 +88,7 @@ export namespace AssemblySymmetryData {
|
||||
export async function fetchRCSB(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
|
||||
const client = new GraphQLClient(props.serverUrl, ctx.assetManager);
|
||||
const variables = {
|
||||
assembly_id: structure.units[0].conformation.operator.assembly?.id || '',
|
||||
assembly_id: structure.units[0].conformation.operator.assembly?.id || 'deposited', // data.rcsb.org guarantees assembly 'deposited' to be present for IHM
|
||||
entry_id: structure.units[0].model.entryId
|
||||
};
|
||||
const result = await client.request(ctx.runtime, rcsb_symmetry_gql, variables);
|
||||
|
||||
@@ -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, '');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const Description = 'Assigns colors to confal pyramids';
|
||||
|
||||
@@ -62,7 +63,7 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
|
||||
export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramidsColorThemeParams, 'confal-pyramids'> = {
|
||||
name: 'confal-pyramids',
|
||||
label: 'Confal Pyramids',
|
||||
category: ColorTheme.Category.Residue,
|
||||
category: ColorThemeCategory.Residue,
|
||||
factory: ConfalPyramidsColorTheme,
|
||||
getParams: getConfalPyramidsColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
|
||||
|
||||
@@ -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, '');
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getColorMapParams } from '../../../mol-util/color/params';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TableLegend } from '../../../mol-util/legend';
|
||||
import { ObjectKeys } from '../../../mol-util/type-helpers';
|
||||
import { ColorThemeCategory } from '../../../mol-theme/color/categories';
|
||||
|
||||
const Description = 'Assigns colors to NtC Tube segments';
|
||||
|
||||
@@ -87,7 +88,7 @@ export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTub
|
||||
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
|
||||
name: 'ntc-tube',
|
||||
label: 'NtC Tube',
|
||||
category: ColorTheme.Category.Residue,
|
||||
category: ColorThemeCategory.Residue,
|
||||
factory: NtCTubeColorTheme,
|
||||
getParams: getNtCTubeColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
|
||||
|
||||
106
src/extensions/interactions/compute.ts
Normal file
106
src/extensions/interactions/compute.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { InteractionsProps } from '../../mol-model-props/computed/interactions';
|
||||
import { FeatureType } from '../../mol-model-props/computed/interactions/common';
|
||||
import { computeInteractions, Interactions } from '../../mol-model-props/computed/interactions/interactions';
|
||||
import { Structure, StructureElement, Unit } from '../../mol-model/structure';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { InteractionInfo, InteractionTypeToKind, StructureInteractions } from './model';
|
||||
|
||||
export interface ComputeInteractionsOptions {
|
||||
interactions?: InteractionsProps
|
||||
}
|
||||
|
||||
export async function computeContacts(
|
||||
ctx: RuntimeContext,
|
||||
selection: readonly { structureRef: string, loci: StructureElement.Loci }[],
|
||||
options?: ComputeInteractionsOptions
|
||||
): Promise<StructureInteractions> {
|
||||
const unitIdToStructureRef = new Map<number, string>();
|
||||
const unitIdToContactGroupId = new Map<number, number>();
|
||||
const units: Unit[] = [];
|
||||
|
||||
let contactGroupId = 0;
|
||||
const builder = Structure.Builder();
|
||||
for (const { structureRef, loci } of selection) {
|
||||
const s = StructureElement.Loci.toStructure(loci);
|
||||
for (const unit of s.units) {
|
||||
const newUnit = builder.copyUnit(unit, { propagateTransientCache: true });
|
||||
units.push(newUnit);
|
||||
unitIdToStructureRef.set(newUnit.id, structureRef);
|
||||
unitIdToContactGroupId.set(newUnit.id, contactGroupId);
|
||||
}
|
||||
|
||||
contactGroupId++;
|
||||
}
|
||||
|
||||
const structure = builder.getStructure();
|
||||
const interactions = await computeInteractions(
|
||||
{ runtime: ctx, assetManager: new AssetManager() },
|
||||
structure,
|
||||
options?.interactions ?? {},
|
||||
{
|
||||
skipIntraContacts: true,
|
||||
unitPairTest: (a, b) => unitIdToContactGroupId.get(a.id) !== unitIdToContactGroupId.get(b.id)
|
||||
}
|
||||
);
|
||||
|
||||
const { edges } = interactions.contacts;
|
||||
const result: StructureInteractions = { kind: 'structure-interactions', elements: [] };
|
||||
for (const e of edges) {
|
||||
if (e.unitA > e.unitB) continue;
|
||||
|
||||
const [a, aType] = processFeature(structure, interactions, e.unitA, e.indexA);
|
||||
const [b] = processFeature(structure, interactions, e.unitB, e.indexB);
|
||||
|
||||
const kind = InteractionTypeToKind[e.props.type] ?? 'unknown';
|
||||
const info: InteractionInfo = { kind };
|
||||
|
||||
if (kind === 'hydrogen-bond' || kind === 'weak-hydrogen-bond') {
|
||||
const isADonor = aType === FeatureType.HydrogenDonor || aType === FeatureType.WeakHydrogenDonor;
|
||||
|
||||
result.elements.push({
|
||||
info,
|
||||
aStructureRef: isADonor ? unitIdToStructureRef.get(e.unitA)! : unitIdToStructureRef.get(e.unitB)!,
|
||||
bStructureRef: isADonor ? unitIdToStructureRef.get(e.unitB)! : unitIdToStructureRef.get(e.unitA)!,
|
||||
a: isADonor ? a : b,
|
||||
b: isADonor ? b : a,
|
||||
});
|
||||
} else {
|
||||
result.elements.push({
|
||||
info,
|
||||
aStructureRef: unitIdToStructureRef.get(e.unitA)!,
|
||||
bStructureRef: unitIdToStructureRef.get(e.unitB)!,
|
||||
a,
|
||||
b,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const _loc = StructureElement.Location.create();
|
||||
function processFeature(structure: Structure, interactions: Interactions, unitId: number, featureIndex: number) {
|
||||
_loc.structure = structure;
|
||||
_loc.unit = structure.unitMap.get(unitId);
|
||||
const xs = interactions.unitsFeatures.get(unitId)!;
|
||||
|
||||
let type: FeatureType = FeatureType.None;
|
||||
const builder = structure.subsetBuilder(false);
|
||||
builder.beginUnit(_loc.unit.id);
|
||||
for (let o = xs.offsets[featureIndex], uIEnd = xs.offsets[featureIndex + 1]; o < uIEnd; o++) {
|
||||
const unitIndex = xs.members[o];
|
||||
_loc.element = _loc.unit.elements[unitIndex];
|
||||
builder.addElement(_loc.element);
|
||||
type = xs.types[o];
|
||||
}
|
||||
builder.commitUnit();
|
||||
|
||||
return [Structure.toStructureElementLoci(builder.getStructure()), type] as const;
|
||||
}
|
||||
35
src/extensions/interactions/custom.ts
Normal file
35
src/extensions/interactions/custom.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, StructureElement } from '../../mol-model/structure';
|
||||
import { InteractionElementSchema, InteractionInfo, StructureInteractionElement, StructureInteractions } from './model';
|
||||
|
||||
export function getCustomInteractionData(interactions: InteractionElementSchema[], structures: { [ref: string]: Structure }): StructureInteractions {
|
||||
const elements: StructureInteractionElement[] = [];
|
||||
|
||||
for (const schema of interactions) {
|
||||
let info: InteractionInfo;
|
||||
if (schema.kind === 'covalent') {
|
||||
info = { kind: schema.kind, degree: schema.degree };
|
||||
} else {
|
||||
info = { kind: schema.kind };
|
||||
}
|
||||
elements.push({
|
||||
sourceSchema: schema,
|
||||
info,
|
||||
aStructureRef: schema.aStructureRef,
|
||||
a: resolveLoci(structures[schema.aStructureRef!], schema.a),
|
||||
bStructureRef: schema.bStructureRef,
|
||||
b: resolveLoci(structures[schema.bStructureRef!], schema.b),
|
||||
});
|
||||
}
|
||||
|
||||
return { kind: 'structure-interactions', elements };
|
||||
}
|
||||
|
||||
function resolveLoci(structure: Structure, schema: StructureElement.Schema) {
|
||||
return StructureElement.Schema.toLoci(structure, schema);
|
||||
}
|
||||
83
src/extensions/interactions/model.ts
Normal file
83
src/extensions/interactions/model.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { InteractionType } from '../../mol-model-props/computed/interactions/common';
|
||||
import { StructureElement } from '../../mol-model/structure';
|
||||
|
||||
interface InteractionElementSchemaBase {
|
||||
aStructureRef?: string,
|
||||
a: StructureElement.Schema,
|
||||
bStructureRef?: string,
|
||||
b: StructureElement.Schema,
|
||||
description?: string,
|
||||
}
|
||||
|
||||
export type InteractionElementSchema =
|
||||
| { kind: 'unknown' } & InteractionElementSchemaBase
|
||||
| { kind: 'ionic' } & InteractionElementSchemaBase
|
||||
| { kind: 'pi-stacking' } & InteractionElementSchemaBase
|
||||
| { kind: 'cation-pi' } & InteractionElementSchemaBase
|
||||
| { kind: 'halogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'weak-hydrogen-bond' } & InteractionElementSchemaBase
|
||||
| { kind: 'hydrophobic' } & InteractionElementSchemaBase
|
||||
| { kind: 'metal-coordination' } & InteractionElementSchemaBase
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 } & InteractionElementSchemaBase
|
||||
|
||||
export type InteractionKind = InteractionElementSchema['kind']
|
||||
|
||||
export const InteractionKinds: InteractionKind[] = [
|
||||
'unknown',
|
||||
'ionic',
|
||||
'pi-stacking',
|
||||
'cation-pi',
|
||||
'halogen-bond',
|
||||
'hydrogen-bond',
|
||||
'weak-hydrogen-bond',
|
||||
'hydrophobic',
|
||||
'metal-coordination',
|
||||
'covalent',
|
||||
];
|
||||
|
||||
export type InteractionInfo =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'ionic' }
|
||||
| { kind: 'pi-stacking' }
|
||||
| { kind: 'cation-pi' }
|
||||
| { kind: 'halogen-bond' }
|
||||
| { kind: 'hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'weak-hydrogen-bond', hydrogenStructureRef?: string, hydrogen?: StructureElement.Loci }
|
||||
| { kind: 'hydrophobic' }
|
||||
| { kind: 'metal-coordination' }
|
||||
| { kind: 'covalent', degree?: 'aromatic' | 1 | 2 | 3 | 4 }
|
||||
|
||||
export interface StructureInteractionElement {
|
||||
// Pass the schema when loading from custom data
|
||||
sourceSchema?: InteractionElementSchema,
|
||||
|
||||
info: InteractionInfo,
|
||||
aStructureRef?: string,
|
||||
a: StructureElement.Loci,
|
||||
bStructureRef?: string,
|
||||
b: StructureElement.Loci,
|
||||
}
|
||||
|
||||
export interface StructureInteractions {
|
||||
kind: 'structure-interactions',
|
||||
elements: StructureInteractionElement[],
|
||||
}
|
||||
|
||||
export const InteractionTypeToKind = {
|
||||
[InteractionType.Unknown]: 'unknown' as InteractionKind,
|
||||
[InteractionType.Ionic]: 'ionic' as InteractionKind,
|
||||
[InteractionType.CationPi]: 'cation-pi' as InteractionKind,
|
||||
[InteractionType.PiStacking]: 'pi-stacking' as InteractionKind,
|
||||
[InteractionType.HydrogenBond]: 'hydrogen-bond' as InteractionKind,
|
||||
[InteractionType.HalogenBond]: 'halogen-bond' as InteractionKind,
|
||||
[InteractionType.Hydrophobic]: 'hydrophobic' as InteractionKind,
|
||||
[InteractionType.MetalCoordination]: 'metal-coordination' as InteractionKind,
|
||||
[InteractionType.WeakHydrogenBond]: 'weak-hydrogen-bond' as InteractionKind,
|
||||
};
|
||||
78
src/extensions/interactions/transforms.ts
Normal file
78
src/extensions/interactions/transforms.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { InteractionsParams } from '../../mol-model-props/computed/interactions';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
|
||||
import { StateTransformer } from '../../mol-state';
|
||||
import { Task } from '../../mol-task';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { computeContacts } from './compute';
|
||||
import { getCustomInteractionData } from './custom';
|
||||
import { InteractionElementSchema, StructureInteractions } from './model';
|
||||
import { buildInteractionsShape, InteractionVisualParams } from './visuals';
|
||||
|
||||
const Factory = StateTransformer.builderFactory('interactions-extension');
|
||||
|
||||
export class InteractionData extends SO.Create<{ interactions: StructureInteractions }>({ name: 'Interactions', typeClass: 'Data' }) { }
|
||||
|
||||
export const ComputeContacts = Factory({
|
||||
name: 'compute-contacts',
|
||||
display: 'Compute Contacts',
|
||||
from: SO.Molecule.Structure.Selections,
|
||||
to: InteractionData,
|
||||
params: {
|
||||
interactions: PD.Group(InteractionsParams),
|
||||
},
|
||||
})({
|
||||
apply({ params, a }) {
|
||||
return Task.create('Compute Contacts', async ctx => {
|
||||
const interactions = await computeContacts(ctx, a.data, { interactions: params.interactions });
|
||||
return new InteractionData({ interactions }, { label: 'Interactions' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const CustomInteractions = Factory({
|
||||
name: 'custom-interactions',
|
||||
display: 'Custom Interactions',
|
||||
from: SO.Root,
|
||||
to: InteractionData,
|
||||
params: {
|
||||
interactions: PD.Value<InteractionElementSchema[]>([], { isHidden: true }),
|
||||
},
|
||||
})({
|
||||
apply({ params, dependencies }) {
|
||||
return Task.create('Custom Interactions', async ctx => {
|
||||
const structures: { [ref: string]: Structure } = {};
|
||||
for (const [k, v] of Object.entries(dependencies ?? {})) {
|
||||
structures[k] = v.data as Structure;
|
||||
}
|
||||
const interactions = getCustomInteractionData(params.interactions, structures);
|
||||
return new InteractionData({ interactions }, { label: 'Custom Interactions' });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
export const InteractionsShape = Factory({
|
||||
name: 'interactions-shape',
|
||||
display: { name: 'Interactions Shape' },
|
||||
from: InteractionData,
|
||||
to: SO.Shape.Provider,
|
||||
params: InteractionVisualParams
|
||||
})({
|
||||
canAutoUpdate: () => true,
|
||||
apply({ a, params }) {
|
||||
return new SO.Shape.Provider({
|
||||
label: 'Interactions Shape Provider',
|
||||
data: { interactions: a.data.interactions, params },
|
||||
params: PD.withDefaults(Mesh.Params, { }),
|
||||
getShape: (_, data: { interactions: StructureInteractions, params: InteractionVisualParams }, __, prev: any) => buildInteractionsShape(data.interactions, data.params, prev?.geometry),
|
||||
geometryUtils: Mesh.Utils,
|
||||
}, { label: 'Interactions Shape Provider' });
|
||||
}
|
||||
});
|
||||
198
src/extensions/interactions/visuals.ts
Normal file
198
src/extensions/interactions/visuals.ts
Normal file
@@ -0,0 +1,198 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../mol-geo/geometry/mesh/builder/cylinder';
|
||||
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Shape } from '../../mol-model/shape';
|
||||
import { StructureElement } from '../../mol-model/structure';
|
||||
import { addLinkCylinderMesh, AddLinkOptions, AddLinkParams, DefaultLinkCylinderProps, LinkStyle } from '../../mol-repr/structure/visual/util/link';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { InteractionKinds, StructureInteractions } from './model';
|
||||
|
||||
function visualParams({ color, style = 'dashed', radius = 0.04 }: { color: Color, style?: 'dashed' | 'solid', radius?: number }) {
|
||||
return PD.Group({
|
||||
color: PD.Color(color),
|
||||
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
|
||||
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
|
||||
});
|
||||
}
|
||||
|
||||
function hydrogenVisualParams({ color, style = 'dashed', radius = 0.04, showArrow = true, arrowOffset = 0.18 }: { color: Color, style?: 'dashed' | 'solid', radius?: number, showArrow?: boolean, arrowOffset?: number }) {
|
||||
return PD.Group({
|
||||
color: PD.Color(color),
|
||||
style: PD.Select<'dashed' | 'solid'>(style, [['dashed', 'Dashed'], ['solid', 'Solid']]),
|
||||
radius: PD.Numeric(radius, { min: 0.01, max: 1, step: 0.01 }),
|
||||
showArrow: PD.Boolean(showArrow),
|
||||
arrowOffset: PD.Numeric(arrowOffset, { min: 0, max: 1, step: 0.001 }),
|
||||
});
|
||||
}
|
||||
|
||||
export const InteractionVisualParams = {
|
||||
kinds: PD.MultiSelect(InteractionKinds, InteractionKinds.map(k => [k, stringToWords(k)])),
|
||||
styles: PD.Group({
|
||||
'unknown': visualParams({ color: Color(0x0) }),
|
||||
'ionic': visualParams({ color: Color(0xADD8E6) }),
|
||||
'pi-stacking': visualParams({ color: Color(0x1E3F66) }),
|
||||
'cation-pi': visualParams({ color: Color(0x06402B) }),
|
||||
'halogen-bond': visualParams({ color: Color(0xFFDE21) }),
|
||||
'hydrogen-bond': hydrogenVisualParams({ color: Color(0x0), style: 'solid' }),
|
||||
'weak-hydrogen-bond': hydrogenVisualParams({ color: Color(0x0) }),
|
||||
'hydrophobic': visualParams({ color: Color(0x555555) }),
|
||||
'metal-coordination': visualParams({ color: Color(0x952e8f) }),
|
||||
'covalent': PD.Group({
|
||||
color: PD.Color(Color(0x999999)),
|
||||
radius: PD.Numeric(0.1, { min: 0.01, max: 1, step: 0.01 }),
|
||||
}),
|
||||
})
|
||||
};
|
||||
|
||||
export type InteractionVisualParams = PD.Values<typeof InteractionVisualParams>;
|
||||
|
||||
export function buildInteractionsShape(interactions: StructureInteractions, params: InteractionVisualParams, prev?: Mesh): Shape<Mesh> {
|
||||
const mesh = MeshBuilder.createState(interactions.elements.length * 128, 1024, prev);
|
||||
|
||||
mesh.currentGroup = -1;
|
||||
const tooltips = new Map<number, string>();
|
||||
|
||||
const visible = new Set(params.kinds);
|
||||
const kindsToWords = new Map(InteractionKinds.map(k => [k, stringToWords(k)]));
|
||||
|
||||
const colors = new Map<number, Color>();
|
||||
|
||||
const bA = { sphere: Sphere3D.zero() };
|
||||
const bB = { sphere: Sphere3D.zero() };
|
||||
|
||||
const pA = Vec3();
|
||||
const pB = Vec3();
|
||||
|
||||
const dir = Vec3();
|
||||
const capPos = Vec3();
|
||||
|
||||
const addLinkOptions: AddLinkOptions = {
|
||||
builderState: mesh,
|
||||
props: { ...DefaultLinkCylinderProps },
|
||||
};
|
||||
const addLinkParams: AddLinkParams = {
|
||||
a: pA,
|
||||
b: pB,
|
||||
group: 0,
|
||||
linkStub: false,
|
||||
linkStyle: LinkStyle.Solid,
|
||||
linkRadius: 0,
|
||||
};
|
||||
|
||||
for (const interaction of interactions.elements) {
|
||||
mesh.currentGroup++;
|
||||
if (!visible.has(interaction.info.kind)) continue;
|
||||
|
||||
let tooltip: string;
|
||||
if (interaction.info.kind === 'covalent') {
|
||||
if (interaction.info.degree === 'aromatic') tooltip = 'Aromatic';
|
||||
else if (interaction.info.degree === 1) tooltip = 'Single';
|
||||
else if (interaction.info.degree === 2) tooltip = 'Double';
|
||||
else if (interaction.info.degree === 3) tooltip = 'Triple';
|
||||
else if (interaction.info.degree === 4) tooltip = 'Quadruple';
|
||||
else tooltip = 'Covalent';
|
||||
} else {
|
||||
tooltip = kindsToWords.get(interaction.info.kind) ?? interaction.info.kind;
|
||||
}
|
||||
if (interaction.sourceSchema?.description) {
|
||||
tooltip += ` (${interaction.sourceSchema.description})`;
|
||||
}
|
||||
tooltips.set(mesh.currentGroup, tooltip);
|
||||
|
||||
const options = params.styles[interaction.info.kind];
|
||||
let style: 'dashed' | 'solid' = 'solid';
|
||||
if (interaction.info.kind !== 'covalent') {
|
||||
style = params.styles[interaction.info.kind].style;
|
||||
}
|
||||
|
||||
colors.set(mesh.currentGroup, params.styles[interaction.info.kind].color);
|
||||
|
||||
StructureElement.Loci.getBoundary(interaction.a, undefined, bA);
|
||||
StructureElement.Loci.getBoundary(interaction.b, undefined, bB);
|
||||
|
||||
Vec3.sub(dir, bB.sphere.center, bA.sphere.center);
|
||||
Vec3.normalize(dir, dir);
|
||||
|
||||
Vec3.copy(pA, bA.sphere.center);
|
||||
Vec3.copy(pB, bB.sphere.center);
|
||||
|
||||
if (interaction.info.kind === 'hydrogen-bond' || interaction.info.kind === 'weak-hydrogen-bond') {
|
||||
const hydrogenStyle = params.styles[interaction.info.kind];
|
||||
if (hydrogenStyle.showArrow && hydrogenStyle.arrowOffset > 0) {
|
||||
Vec3.scaleAndAdd(pB, pB, dir, -hydrogenStyle.arrowOffset);
|
||||
}
|
||||
|
||||
if (hydrogenStyle.showArrow) {
|
||||
const height = options.radius * 3;
|
||||
Vec3.scaleAndAdd(capPos, pB, dir, -height);
|
||||
cylinder(mesh, pA, capPos, options.radius, style);
|
||||
addSimpleCylinder(
|
||||
mesh,
|
||||
capPos,
|
||||
pB,
|
||||
{ radiusTop: 0, radiusBottom: height, topCap: false, bottomCap: true }
|
||||
);
|
||||
} else {
|
||||
cylinder(mesh, pA, pB, options.radius, style);
|
||||
}
|
||||
} else {
|
||||
if (interaction.info.kind !== 'covalent') {
|
||||
cylinder(mesh, pA, pB, options.radius, style);
|
||||
} else {
|
||||
addLinkParams.group = mesh.currentGroup;
|
||||
addLinkParams.linkRadius = options.radius;
|
||||
const degree = interaction.info.degree ?? 1;
|
||||
if (degree === 'aromatic') addLinkParams.linkStyle = LinkStyle.Aromatic;
|
||||
else if (degree === 2) addLinkParams.linkStyle = LinkStyle.Double;
|
||||
else if (degree === 3) addLinkParams.linkStyle = LinkStyle.Triple;
|
||||
else addLinkParams.linkStyle = LinkStyle.Solid;
|
||||
addLinkParams.a = pA;
|
||||
addLinkParams.b = pB;
|
||||
addLinkCylinderMesh(addLinkOptions, addLinkParams);
|
||||
addLinkParams.a = pB;
|
||||
addLinkParams.b = pA;
|
||||
addLinkCylinderMesh(addLinkOptions, addLinkParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Shape.create(
|
||||
'Interactions',
|
||||
interactions,
|
||||
MeshBuilder.getMesh(mesh),
|
||||
(g) => colors.get(g) ?? 0 as Color,
|
||||
(g) => 1,
|
||||
(g) => tooltips.get(g) ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function cylinder(mesh: MeshBuilder.State, a: Vec3, b: Vec3, radius: number, style: 'dashed' | 'solid') {
|
||||
const props: BasicCylinderProps = {
|
||||
radiusBottom: radius,
|
||||
radiusTop: radius,
|
||||
topCap: true,
|
||||
bottomCap: true,
|
||||
};
|
||||
|
||||
if (style === 'dashed') {
|
||||
const dist = Vec3.distance(a, b);
|
||||
const count = Math.ceil(dist / (2 * radius));
|
||||
addFixedCountDashedCylinder(mesh, a, b, 1.0, count, true, props);
|
||||
} else {
|
||||
if (style !== 'solid') {
|
||||
console.warn(`Unknown style '${style}', using 'solid' instead.`);
|
||||
}
|
||||
addSimpleCylinder(mesh, a, b, props);
|
||||
}
|
||||
}
|
||||
148
src/extensions/json-cif/_spec/json-cif.spec.ts
Normal file
148
src/extensions/json-cif/_spec/json-cif.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { molfileToJSONCif } from '../utils';
|
||||
import { CifFile } from '../../../mol-io/reader/cif';
|
||||
import { trajectoryFromMmCIF } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { JSONCifLigandGraph } from '../ligand-graph';
|
||||
import { parseJSONCif } from '../parser';
|
||||
import { JSONCifDataBlock } from '../model';
|
||||
|
||||
describe('json-cif', () => {
|
||||
it('roundtrips', async () => {
|
||||
const { structure, jsoncif: file } = await molfileToJSONCif(MolString);
|
||||
|
||||
expect(file.dataBlocks.length).toBe(1);
|
||||
expect(file.dataBlocks[0].categoryNames.length).toBe(2);
|
||||
expect(file.dataBlocks[0].categoryNames[0]).toBe('atom_site');
|
||||
expect(file.dataBlocks[0].categories['atom_site'].rows.length).toBe(structure.elementCount);
|
||||
|
||||
const parsed = parseJSONCif(file);
|
||||
const parsedModel = await parseCifModel(parsed);
|
||||
expect(parsedModel.atomicHierarchy.atoms._rowCount).toBe(structure.elementCount);
|
||||
});
|
||||
|
||||
it('ligand graph', async () => {
|
||||
const { structure, jsoncif: file } = await molfileToJSONCif(MolString);
|
||||
|
||||
// remove atom
|
||||
let graph = new JSONCifLigandGraph(file.dataBlocks[0]);
|
||||
graph.removeAtom(graph.atoms[0]);
|
||||
let data = graph.getData().block;
|
||||
expect(data.categories.atom_site.rows.length).toBe(structure.elementCount - 1);
|
||||
|
||||
// modify atom
|
||||
graph = new JSONCifLigandGraph(file.dataBlocks[0]);
|
||||
expect(file.dataBlocks[0].categories.atom_site.rows[0].type_symbol !== 'N').toBe(true);
|
||||
graph.modifyAtom(1, { type_symbol: 'N' });
|
||||
data = graph.getData().block;
|
||||
expect(data.categories.atom_site.rows[0].type_symbol).toBe('N');
|
||||
|
||||
// add atom and bond
|
||||
graph = new JSONCifLigandGraph(file.dataBlocks[0]);
|
||||
const newAtom = graph.addAtom({ type_symbol: 'C', Cartn_x: 0, Cartn_y: 0, Cartn_z: 0 });
|
||||
graph.addOrUpdateBond(graph.atoms[0], newAtom, { value_order: 'sing', type_id: 'covale' });
|
||||
data = graph.getData().block;
|
||||
expect(data.categories.atom_site.rows.length).toBe(structure.elementCount + 1);
|
||||
expect(data.categories.molstar_bond_site.rows.length).toBe(file.dataBlocks[0].categories.molstar_bond_site.rows.length + 1);
|
||||
|
||||
// remove bond
|
||||
graph.removeBond(graph.atoms[0], newAtom);
|
||||
data = graph.getData().block;
|
||||
expect(data.categories.atom_site.rows.length).toBe(structure.elementCount + 1);
|
||||
expect(data.categories.molstar_bond_site.rows.length).toBe(file.dataBlocks[0].categories.molstar_bond_site.rows.length);
|
||||
});
|
||||
|
||||
it('ligand graph traversal', () => {
|
||||
const data: JSONCifDataBlock = {
|
||||
header: 'test',
|
||||
categoryNames: ['atom_site', 'molstar_bond_site'],
|
||||
categories: {
|
||||
atom_site: {
|
||||
name: 'atom_site',
|
||||
fieldNames: ['id', 'type_symbol'],
|
||||
rows: [
|
||||
{ id: 1, type_symbol: 'C', Cartn_x: 0, Cartn_y: 0, Cartn_z: 0 },
|
||||
{ id: 2, type_symbol: 'C', Cartn_x: 1, Cartn_y: 0, Cartn_z: 0 },
|
||||
{ id: 3, type_symbol: 'C', Cartn_x: 2, Cartn_y: 0, Cartn_z: 0 },
|
||||
{ id: 4, type_symbol: 'C', Cartn_x: 2, Cartn_y: 0, Cartn_z: 0 },
|
||||
],
|
||||
},
|
||||
molstar_bond_site: {
|
||||
name: 'molstar_bond_site',
|
||||
fieldNames: ['atom_id_1', 'atom_id_2'],
|
||||
rows: [
|
||||
{ atom_id_1: 1, atom_id_2: 4 },
|
||||
{ atom_id_1: 1, atom_id_2: 2 },
|
||||
{ atom_id_1: 2, atom_id_2: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const graph = new JSONCifLigandGraph(data);
|
||||
|
||||
const bfs = graph.traverse(1, 'bfs', [] as number[], (a, s) => s.push(a.row.id!));
|
||||
expect(bfs).toEqual([1, 4, 2, 3]);
|
||||
const dfs = graph.traverse(1, 'dfs', [] as number[], (a, s) => s.push(a.row.id!));
|
||||
expect(dfs).toEqual([1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
async function parseCifModel(file: CifFile) {
|
||||
const models = await trajectoryFromMmCIF(file.blocks[0], file).run();
|
||||
const model = await Task.resolveInContext(models.getFrameAtIndex(0));
|
||||
return model;
|
||||
}
|
||||
|
||||
const MolString = `2244
|
||||
-OEChem-04072009073D
|
||||
|
||||
21 21 0 0 0 0 0 0 0999 V2000
|
||||
1.2333 0.5540 0.7792 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.6952 -2.7148 -0.7502 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
0.7958 -2.1843 0.8685 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1.7813 0.8105 -1.4821 O 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.0857 0.6088 0.4403 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7927 -0.5515 0.1244 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.7288 1.8464 0.4133 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.1426 -0.4741 -0.2184 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.0787 1.9238 0.0706 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7855 0.7636 -0.2453 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1409 -1.8536 0.1477 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
2.1094 0.6715 -0.3113 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.5305 0.5996 0.1635 C 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.1851 2.7545 0.6593 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.7247 -1.3605 -0.4564 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-2.5797 2.8872 0.0506 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-3.8374 0.8238 -0.5090 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7290 1.4184 0.8593 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
4.2045 0.6969 -0.6924 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
3.7105 -0.3659 0.6426 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
-0.2555 -3.5916 -0.7337 H 0 0 0 0 0 0 0 0 0 0 0 0
|
||||
1 5 1 0 0 0 0
|
||||
1 12 1 0 0 0 0
|
||||
2 11 1 0 0 0 0
|
||||
2 21 1 0 0 0 0
|
||||
3 11 2 0 0 0 0
|
||||
4 12 2 0 0 0 0
|
||||
5 6 1 0 0 0 0
|
||||
5 7 2 0 0 0 0
|
||||
6 8 2 0 0 0 0
|
||||
6 11 1 0 0 0 0
|
||||
7 9 1 0 0 0 0
|
||||
7 14 1 0 0 0 0
|
||||
8 10 1 0 0 0 0
|
||||
8 15 1 0 0 0 0
|
||||
9 10 2 0 0 0 0
|
||||
9 16 1 0 0 0 0
|
||||
10 17 1 0 0 0 0
|
||||
12 13 1 0 0 0 0
|
||||
13 18 1 0 0 0 0
|
||||
13 19 1 0 0 0 0
|
||||
13 20 1 0 0 0 0
|
||||
M END`;
|
||||
114
src/extensions/json-cif/encoder.ts
Normal file
114
src/extensions/json-cif/encoder.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../mol-data/db';
|
||||
import { Category, Encoder } from '../../mol-io/writer/cif/encoder';
|
||||
import { BinaryEncodingProvider } from '../../mol-io/writer/cif/encoder/binary';
|
||||
import { getCategoryInstanceData, getIncludedFields } from '../../mol-io/writer/cif/encoder/util';
|
||||
import { Writer } from '../../mol-io/writer/writer';
|
||||
import { JSONCifCategory, JSONCifDataBlock, JSONCifFile, JSONCifVERSION } from './model';
|
||||
|
||||
export class JSONCifEncoder implements Encoder<string> {
|
||||
private data: JSONCifFile;
|
||||
private dataBlocks: JSONCifDataBlock[] = [];
|
||||
private encodedData: string | undefined;
|
||||
private filter: Category.Filter = Category.DefaultFilter;
|
||||
|
||||
readonly isBinary = false;
|
||||
readonly binaryEncodingProvider: BinaryEncodingProvider | undefined;
|
||||
|
||||
setFilter(filter?: Category.Filter) {
|
||||
this.filter = filter || Category.DefaultFilter;
|
||||
}
|
||||
|
||||
isCategoryIncluded(name: string) {
|
||||
return this.filter.includeCategory(name);
|
||||
}
|
||||
|
||||
setFormatter(formatter?: Category.Formatter) {
|
||||
// No formatter needed for JSON encoding.
|
||||
}
|
||||
|
||||
startDataBlock(header: string) {
|
||||
this.dataBlocks.push({
|
||||
header: (header || '').replace(/[ \n\t]/g, '').toUpperCase(),
|
||||
categoryNames: [],
|
||||
categories: {}
|
||||
});
|
||||
}
|
||||
|
||||
writeCategory<Ctx>(category: Category<Ctx>, context?: Ctx, options?: Encoder.WriteCategoryOptions) {
|
||||
if (this.encodedData) {
|
||||
throw new Error('The writer contents have already been encoded, no more writing.');
|
||||
}
|
||||
|
||||
if (!this.dataBlocks.length) {
|
||||
throw new Error('No data block created.');
|
||||
}
|
||||
|
||||
if (!options?.ignoreFilter && !this.filter.includeCategory(category.name)) return;
|
||||
|
||||
const { instance, rowCount, source } = getCategoryInstanceData(category, context);
|
||||
if (!rowCount) return;
|
||||
|
||||
const fields = getIncludedFields(instance);
|
||||
if (!fields.length) return;
|
||||
|
||||
const rows: Record<string, any>[] = [];
|
||||
const cat: JSONCifCategory = { name: category.name, fieldNames: fields.map(f => f.name), rows };
|
||||
|
||||
for (const src of source) {
|
||||
const d = src.data;
|
||||
const keys = src.keys();
|
||||
while (keys.hasNext) {
|
||||
const row: Record<string, any> = {};
|
||||
const k = keys.move();
|
||||
for (const f of fields) {
|
||||
const kind = f.valueKind ? f.valueKind(k, d) : Column.ValueKinds.Present;
|
||||
if (kind === Column.ValueKinds.Present) {
|
||||
row[f.name] = f.value(k, d, rows.length);
|
||||
} else if (kind === Column.ValueKinds.NotPresent) {
|
||||
row[f.name] = null;
|
||||
}
|
||||
}
|
||||
cat.rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
this.dataBlocks[this.dataBlocks.length - 1].categoryNames.push(cat.name);
|
||||
this.dataBlocks[this.dataBlocks.length - 1].categories[cat.name] = cat;
|
||||
}
|
||||
|
||||
encode() {
|
||||
if (this.encodedData) return;
|
||||
this.encodedData = this.options?.formatJSON ? JSON.stringify(this.data, null, 2) : JSON.stringify(this.data);
|
||||
}
|
||||
|
||||
writeTo(writer: Writer) {
|
||||
writer.writeString(this.encodedData!);
|
||||
}
|
||||
|
||||
getData() {
|
||||
this.encode();
|
||||
return this.encodedData!;
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this.encodedData?.length ?? 0;
|
||||
}
|
||||
|
||||
getFile() {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
constructor(encoder: string, public options?: { formatJSON?: boolean }) {
|
||||
this.data = {
|
||||
encoder,
|
||||
version: JSONCifVERSION,
|
||||
dataBlocks: this.dataBlocks
|
||||
};
|
||||
}
|
||||
}
|
||||
307
src/extensions/json-cif/ligand-graph.ts
Normal file
307
src/extensions/json-cif/ligand-graph.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Table } from '../../mol-data/db';
|
||||
import { mmCIF_Schema } from '../../mol-io/reader/cif/schema/mmcif';
|
||||
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { MolstarBondSiteTypeId, MolstarBondSiteValueOrder } from '../../mol-model/structure/export/categories/molstar_bond_site';
|
||||
import { UUID } from '../../mol-util';
|
||||
import { arrayMapAdd } from '../../mol-util/map';
|
||||
import { JSONCifDataBlock } from './model';
|
||||
|
||||
type Atom = Partial<Table.Row<mmCIF_Schema['atom_site']>>
|
||||
|
||||
export interface JSONCifLigandGraphBondProps {
|
||||
value_order: MolstarBondSiteValueOrder | undefined;
|
||||
type_id: MolstarBondSiteTypeId | undefined;
|
||||
}
|
||||
|
||||
export interface JSONCifLigandGraphAtom {
|
||||
key: string;
|
||||
final_id: number | undefined;
|
||||
row: Atom;
|
||||
}
|
||||
|
||||
export interface JSONCifLigandGraphBond {
|
||||
atom_1: JSONCifLigandGraphAtom;
|
||||
atom_2: JSONCifLigandGraphAtom;
|
||||
props: JSONCifLigandGraphBondProps;
|
||||
}
|
||||
|
||||
export interface JSONCifLigandGraphData {
|
||||
block: JSONCifDataBlock;
|
||||
atomIdRemapping: Map<number, number>;
|
||||
addedAtomIds: number[];
|
||||
removedAtomIds: number[];
|
||||
}
|
||||
|
||||
const _State = {
|
||||
p1: Vec3(),
|
||||
p2: Vec3(),
|
||||
};
|
||||
|
||||
export class JSONCifLigandGraph {
|
||||
readonly atoms: JSONCifLigandGraphAtom[] = [];
|
||||
readonly atomsByKey: Map<string, JSONCifLigandGraphAtom> = new Map();
|
||||
readonly atomsById: Map<number, JSONCifLigandGraphAtom> = new Map();
|
||||
/** Bond with the provided key is always atom_1 */
|
||||
readonly bondByKey: Map<string, JSONCifLigandGraphBond[]> = new Map();
|
||||
readonly removedAtomIds: Set<number> = new Set();
|
||||
|
||||
getAtomAtIndex(index: number) {
|
||||
return this.atoms[index];
|
||||
}
|
||||
|
||||
getAtom(atomOrId: number | JSONCifLigandGraphAtom) {
|
||||
return typeof atomOrId === 'number' ? this.atomsById.get(atomOrId) : atomOrId;
|
||||
}
|
||||
|
||||
getBonds(atomOrId: number | JSONCifLigandGraphAtom) {
|
||||
const atom = this.getAtom(atomOrId);
|
||||
if (!atom) return [];
|
||||
return this.bondByKey.get(atom.key) ?? [];
|
||||
}
|
||||
|
||||
getAtomCoords(atomOrId: number | JSONCifLigandGraphAtom, out: Vec3 = Vec3()) {
|
||||
const atom = this.getAtom(atomOrId);
|
||||
if (!atom) return out;
|
||||
const { Cartn_x, Cartn_y, Cartn_z } = atom.row;
|
||||
return Vec3.set(out, Cartn_x!, Cartn_y!, Cartn_z!);
|
||||
}
|
||||
|
||||
getBondDirection(bond: JSONCifLigandGraphBond, out: Vec3 = Vec3()) {
|
||||
const a1 = this.getAtomCoords(bond.atom_1, _State.p1);
|
||||
const a2 = this.getAtomCoords(bond.atom_2, _State.p2);
|
||||
const dir = Vec3.sub(out, a2, a1);
|
||||
return dir;
|
||||
}
|
||||
|
||||
modifyAtom(atomOrId: number | JSONCifLigandGraphAtom, data: Omit<Atom, 'id'>) {
|
||||
const atom = this.getAtom(atomOrId);
|
||||
if (!atom) return;
|
||||
atom.row = { ...atom.row, ...data, id: atom.row.id };
|
||||
}
|
||||
|
||||
addAtom(data: Omit<Atom, 'id'>) {
|
||||
const atom: JSONCifLigandGraphAtom = {
|
||||
key: UUID.create22(),
|
||||
final_id: undefined,
|
||||
row: { ...data, id: undefined },
|
||||
};
|
||||
this.atomsByKey.set(atom.key, atom);
|
||||
this.atoms.push(atom);
|
||||
return atom;
|
||||
}
|
||||
|
||||
removeAtom(atomOrId: number | JSONCifLigandGraphAtom) {
|
||||
const atom = this.getAtom(atomOrId);
|
||||
if (!atom) return;
|
||||
if (typeof atom.row.id === 'number') {
|
||||
this.removedAtomIds.add(atom.row.id);
|
||||
this.atomsById.delete(atom.row.id);
|
||||
}
|
||||
|
||||
this.atoms.splice(this.atoms.indexOf(atom), 1);
|
||||
this.atomsByKey.delete(atom.key);
|
||||
|
||||
const bonds = this.bondByKey.get(atom.key);
|
||||
if (!bonds) return;
|
||||
this.bondByKey.delete(atom.key);
|
||||
|
||||
for (const b of bonds) {
|
||||
const bBonds = this.bondByKey.get(b.atom_2.key);
|
||||
if (!bBonds) continue;
|
||||
this.bondByKey.set(b.atom_2.key, bBonds.filter(bb => bb.atom_2 !== atom));
|
||||
}
|
||||
}
|
||||
|
||||
addOrUpdateBond(atom1: number | JSONCifLigandGraphAtom, atom2: number | JSONCifLigandGraphAtom, props: JSONCifLigandGraphBondProps) {
|
||||
const a1 = this.getAtom(atom1);
|
||||
const a2 = this.getAtom(atom2);
|
||||
if (!a1 || !a2) return;
|
||||
|
||||
const ps = { ...props };
|
||||
this.removeBond(atom1, atom2);
|
||||
arrayMapAdd(this.bondByKey, a1.key, { atom_1: a1, atom_2: a2, props: ps });
|
||||
arrayMapAdd(this.bondByKey, a2.key, { atom_1: a2, atom_2: a1, props: ps });
|
||||
}
|
||||
|
||||
removeBond(atom1: number | JSONCifLigandGraphAtom, atom2: number | JSONCifLigandGraphAtom) {
|
||||
const a1 = this.getAtom(atom1);
|
||||
const a2 = this.getAtom(atom2);
|
||||
if (!a1 || !a2) return;
|
||||
const a1Bonds = this.bondByKey.get(a1.key);
|
||||
if (a1Bonds) {
|
||||
this.bondByKey.set(a1.key, a1Bonds.filter(b => b.atom_2 !== a2));
|
||||
}
|
||||
const a2Bonds = this.bondByKey.get(a2.key);
|
||||
if (a2Bonds) {
|
||||
this.bondByKey.set(a2.key, a2Bonds.filter(b => b.atom_2 !== a1));
|
||||
}
|
||||
}
|
||||
|
||||
private transformAtomCoords(xform: Mat4, atomOrId: number | JSONCifLigandGraphAtom) {
|
||||
const atom = this.getAtom(atomOrId);
|
||||
if (!atom) return;
|
||||
const p = this.getAtomCoords(atom, _State.p1);
|
||||
Vec3.transformMat4(p, p, xform);
|
||||
atom.row.Cartn_x = p[0];
|
||||
atom.row.Cartn_y = p[1];
|
||||
atom.row.Cartn_z = p[2];
|
||||
}
|
||||
|
||||
transformCoords(xform: Mat4, atoms?: (number | JSONCifLigandGraphAtom)[]) {
|
||||
for (const a of atoms ?? this.atoms) {
|
||||
this.transformAtomCoords(xform, a);
|
||||
}
|
||||
}
|
||||
|
||||
traverse<S>(
|
||||
atomOrId: number | JSONCifLigandGraphAtom,
|
||||
how: 'dfs' | 'bfs',
|
||||
state: S,
|
||||
visitAtom: (atom: JSONCifLigandGraphAtom, state: S, pred: JSONCifLigandGraphBond | undefined, graph: JSONCifLigandGraph) => void,
|
||||
): S {
|
||||
const start = this.getAtom(atomOrId);
|
||||
if (!start) return state;
|
||||
|
||||
const visited = new Set<string>();
|
||||
const pred = new Map<string, JSONCifLigandGraphBond>();
|
||||
const queue: string[] = [start.key];
|
||||
|
||||
while (queue.length) {
|
||||
const key = how === 'bfs' ? queue.shift()! : queue.pop()!;
|
||||
if (visited.has(key)) continue;
|
||||
|
||||
const a = this.atomsByKey.get(key)!;
|
||||
visited.add(a.key);
|
||||
visitAtom(a, state, pred.get(key), this);
|
||||
|
||||
const bs = this.bondByKey.get(a.key);
|
||||
if (!bs?.length) continue;
|
||||
for (const b of bs) {
|
||||
if (visited.has(b.atom_2.key)) continue;
|
||||
queue.push(b.atom_2.key);
|
||||
pred.set(b.atom_2.key, b);
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
getData(): JSONCifLigandGraphData {
|
||||
const atomIdRemapping = new Map<number, number>();
|
||||
const addedAtomIds: number[] = [];
|
||||
|
||||
const sortedAtoms = this.atoms.map((a, i) => [a, i] as const);
|
||||
sortedAtoms.sort((a, b) => {
|
||||
const x = a[0].row.type_symbol;
|
||||
const y = b[0].row.type_symbol;
|
||||
if (x === 'H' && y !== 'H') return 1;
|
||||
if (x !== 'H' && y === 'H') return -1;
|
||||
return a[1] - b[1];
|
||||
});
|
||||
|
||||
const atoms: Atom[] = [];
|
||||
|
||||
for (let i = 0; i < sortedAtoms.length; ++i) {
|
||||
const a = sortedAtoms[i][0];
|
||||
const id = i + 1;
|
||||
|
||||
if (a.row.id === undefined) {
|
||||
addedAtomIds.push(id);
|
||||
} else {
|
||||
atomIdRemapping.set(a.row.id!, id);
|
||||
}
|
||||
|
||||
a.final_id = id;
|
||||
atoms.push({ ...a.row, id });
|
||||
}
|
||||
|
||||
const block: JSONCifDataBlock = {
|
||||
...this.data,
|
||||
categories: {
|
||||
...this.data.categories,
|
||||
atom_site: {
|
||||
...this.data.categories['atom_site'],
|
||||
rows: atoms,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const bonds: Record<string, any>[] = [];
|
||||
for (const [a] of sortedAtoms) {
|
||||
const xs = this.bondByKey.get(a.key);
|
||||
if (!xs) continue;
|
||||
for (const bb of xs) {
|
||||
if (a.final_id! >= bb.atom_2.final_id!) continue;
|
||||
|
||||
bonds.push({
|
||||
atom_id_1: a.final_id,
|
||||
atom_id_2: bb.atom_2.final_id,
|
||||
value_order: bb.props.value_order,
|
||||
type_id: bb.props.type_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
bonds.sort((a, b) => {
|
||||
if (a.atom_id_1 !== b.atom_id_1) return a.atom_id_1 - b.atom_id_1;
|
||||
if (a.atom_id_2 !== b.atom_id_2) return a.atom_id_2 - b.atom_id_2;
|
||||
return 0;
|
||||
});
|
||||
|
||||
if (block.categories.molstar_bond_site) {
|
||||
block.categories['molstar_bond_site'] = {
|
||||
...block.categories['molstar_bond_site'],
|
||||
rows: bonds
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
block,
|
||||
atomIdRemapping,
|
||||
addedAtomIds,
|
||||
removedAtomIds: Array.from(this.removedAtomIds).sort((a, b) => a - b),
|
||||
};
|
||||
}
|
||||
|
||||
constructor(private data: JSONCifDataBlock) {
|
||||
for (const row of data.categories['atom_site'].rows) {
|
||||
const atom: JSONCifLigandGraphAtom = {
|
||||
key: UUID.create22(),
|
||||
final_id: row.final_id,
|
||||
row: { ...row },
|
||||
};
|
||||
this.atoms.push(atom);
|
||||
this.atomsByKey.set(atom.key, atom);
|
||||
this.atomsById.set(row.id, atom);
|
||||
}
|
||||
|
||||
if (!data.categories.molstar_bond_site) return;
|
||||
|
||||
for (const row of data.categories.molstar_bond_site.rows) {
|
||||
const atom_1 = this.atomsById.get(row.atom_id_1);
|
||||
const atom_2 = this.atomsById.get(row.atom_id_2);
|
||||
if (!atom_1 || !atom_2) continue;
|
||||
|
||||
arrayMapAdd(this.bondByKey, atom_1.key, {
|
||||
atom_1: atom_1,
|
||||
atom_2: atom_2,
|
||||
props: {
|
||||
value_order: row.value_order,
|
||||
type_id: row.type_id,
|
||||
},
|
||||
});
|
||||
arrayMapAdd(this.bondByKey, atom_2.key, {
|
||||
atom_1: atom_2,
|
||||
atom_2: atom_1,
|
||||
props: {
|
||||
value_order: row.value_order,
|
||||
type_id: row.type_id,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
31
src/extensions/json-cif/model.ts
Normal file
31
src/extensions/json-cif/model.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Copyright (c) 2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
|
||||
import { Table } from '../../mol-data/db';
|
||||
|
||||
export const JSONCifVERSION = '0.1.0';
|
||||
|
||||
export interface JSONCifFile {
|
||||
version: string;
|
||||
encoder: string;
|
||||
dataBlocks: JSONCifDataBlock[];
|
||||
}
|
||||
|
||||
export interface JSONCifDataBlock {
|
||||
header: string,
|
||||
categoryNames: string[],
|
||||
categories: Record<string, JSONCifCategory>,
|
||||
}
|
||||
|
||||
export interface JSONCifCategory<T extends Record<string, any> = Record<string, any>> {
|
||||
name: string,
|
||||
fieldNames: string[],
|
||||
rows: T[],
|
||||
}
|
||||
|
||||
export function getJSONCifCategory<S extends Table.Schema>(block: JSONCifDataBlock, name: string): JSONCifCategory<Table.Row<S>> | undefined {
|
||||
return block.categories[name] as any;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user