mirror of
https://github.com/molstar/molstar.git
synced 2026-06-06 14:44:22 +08:00
Compare commits
391 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e05bd5f0c5 | ||
|
|
a3cdc2844e | ||
|
|
a51947637c | ||
|
|
00ca25ffd7 | ||
|
|
2f0230dc84 | ||
|
|
45522ad410 | ||
|
|
ede1a8da07 | ||
|
|
0199afd5f3 | ||
|
|
f8bda3617f | ||
|
|
bffd7d75e0 | ||
|
|
90a4e019ac | ||
|
|
6edbae80db | ||
|
|
aac0abed32 | ||
|
|
6805dd7947 | ||
|
|
6c9254f2b6 | ||
|
|
897d443873 | ||
|
|
f276ea2258 | ||
|
|
58d735996e | ||
|
|
72b66367f3 | ||
|
|
c592a3b93d | ||
|
|
e773824fb5 | ||
|
|
5d139b6db8 | ||
|
|
4598841ddc | ||
|
|
728414366d | ||
|
|
1b7b38b47e | ||
|
|
ef17cb2cca | ||
|
|
b7b52f5c7d | ||
|
|
79ed8e7de4 | ||
|
|
c4a4562d82 | ||
|
|
d238b8aee9 | ||
|
|
22a57d8f48 | ||
|
|
119e8f63eb | ||
|
|
e6a4122d1c | ||
|
|
9ba5f1f540 | ||
|
|
0a0ac7ee63 | ||
|
|
7670df04ae | ||
|
|
98744af872 | ||
|
|
f4eb509887 | ||
|
|
78874c0024 | ||
|
|
c4bc16fe5d | ||
|
|
988cee0047 | ||
|
|
bf0707a2aa | ||
|
|
9a2191e1cc | ||
|
|
36ead9dda3 | ||
|
|
71e1bb849e | ||
|
|
975cceed77 | ||
|
|
82065dc5b7 | ||
|
|
f0d649f265 | ||
|
|
44ce5df136 | ||
|
|
b00bce69fd | ||
|
|
e2e9e5f6fc | ||
|
|
745746f243 | ||
|
|
b5f229ba6d | ||
|
|
d5a47e617a | ||
|
|
a200ca5b21 | ||
|
|
65b52c8ecd | ||
|
|
492494033f | ||
|
|
9de6d86a0f | ||
|
|
ab34a59677 | ||
|
|
7a96cdd52d | ||
|
|
65cad5ea4d | ||
|
|
a765ba8e3b | ||
|
|
8594ce80a9 | ||
|
|
915797c4a4 | ||
|
|
44c69f538b | ||
|
|
b53a52b04d | ||
|
|
e4396039fd | ||
|
|
e548a3ed85 | ||
|
|
fc44e66b26 | ||
|
|
98f3f5a23b | ||
|
|
f2f10d0cb5 | ||
|
|
aed1056d6c | ||
|
|
be47ac09c9 | ||
|
|
d5e7797a40 | ||
|
|
0aeac628c7 | ||
|
|
668d617cd7 | ||
|
|
62ed993f0d | ||
|
|
aa0a008a41 | ||
|
|
89f01f202d | ||
|
|
733190f7a0 | ||
|
|
50429aacfa | ||
|
|
fa541bdbd3 | ||
|
|
77d173afed | ||
|
|
a934001ae8 | ||
|
|
e5d4606437 | ||
|
|
fb16cd0070 | ||
|
|
c427549b8d | ||
|
|
310300bde8 | ||
|
|
11604b9e8f | ||
|
|
cc1bf482f2 | ||
|
|
61a351b3d4 | ||
|
|
9e91a242bf | ||
|
|
c3daa1a162 | ||
|
|
fe086fb62e | ||
|
|
c2217829a3 | ||
|
|
6333c8073f | ||
|
|
2801bcf111 | ||
|
|
8a2461e157 | ||
|
|
0a081e2a8a | ||
|
|
700a3fe95c | ||
|
|
febc634d8b | ||
|
|
0105f75bb6 | ||
|
|
4cc2073eaa | ||
|
|
9ac204cb6e | ||
|
|
73378bbe9d | ||
|
|
9b5fd2595c | ||
|
|
bca2073ed0 | ||
|
|
f264e4d6b8 | ||
|
|
795222b5b4 | ||
|
|
25eb4450ad | ||
|
|
140df13dae | ||
|
|
792cd513a8 | ||
|
|
14e6172c33 | ||
|
|
911433e056 | ||
|
|
6b585cf0d6 | ||
|
|
86211aaf3a | ||
|
|
071623f5b6 | ||
|
|
21e514ec1e | ||
|
|
9bc0ab12e7 | ||
|
|
1d1bd05400 | ||
|
|
faa750bbf9 | ||
|
|
e92e5c5cef | ||
|
|
b49230ea1f | ||
|
|
44ebc1d39a | ||
|
|
8d8e45f4ce | ||
|
|
898d877aa1 | ||
|
|
85dba9b1a4 | ||
|
|
6b5e90c5fa | ||
|
|
e231fbf3d7 | ||
|
|
0ee8525b2d | ||
|
|
106ee614e7 | ||
|
|
34056751f9 | ||
|
|
1afea8a86a | ||
|
|
96d5bf2447 | ||
|
|
f9265a7049 | ||
|
|
5c57137890 | ||
|
|
4e71618d0f | ||
|
|
de660cc233 | ||
|
|
616a1dabfa | ||
|
|
46ea39703f | ||
|
|
6cf20d0c44 | ||
|
|
0737e23b70 | ||
|
|
70d0c15d28 | ||
|
|
9272c8c5ec | ||
|
|
a3349f82fc | ||
|
|
4d399edbdd | ||
|
|
64598eba96 | ||
|
|
aa25874775 | ||
|
|
dccc06d497 | ||
|
|
c000526cf8 | ||
|
|
2166ab455c | ||
|
|
4de9ce01fc | ||
|
|
f543fd5683 | ||
|
|
8535013ee5 | ||
|
|
320ab77f8e | ||
|
|
982feef0c6 | ||
|
|
bd6d04cefb | ||
|
|
5e1c351efc | ||
|
|
61a294c889 | ||
|
|
71fbd6baab | ||
|
|
33430a836a | ||
|
|
f428e9f39e | ||
|
|
2d26425cbe | ||
|
|
f6030aee25 | ||
|
|
609e03f7d2 | ||
|
|
ba12a8bbee | ||
|
|
947f293844 | ||
|
|
fbff0e769c | ||
|
|
3798223d39 | ||
|
|
ac9c23dc65 | ||
|
|
096f492ccb | ||
|
|
ba96da9354 | ||
|
|
6c1d17bac5 | ||
|
|
ad2ccf4e07 | ||
|
|
dc1b7b4693 | ||
|
|
59e4e2b31d | ||
|
|
d2483dc449 | ||
|
|
d26946e9ee | ||
|
|
cd045a6b48 | ||
|
|
2407729d27 | ||
|
|
1aa22b9fa0 | ||
|
|
35c9f39a69 | ||
|
|
7dd420cc18 | ||
|
|
1d434c259a | ||
|
|
6d193edd68 | ||
|
|
9bf859d6ed | ||
|
|
207230d565 | ||
|
|
b7a673f38e | ||
|
|
2204e4e0d0 | ||
|
|
6276365766 | ||
|
|
505b04c92d | ||
|
|
fc84dcb037 | ||
|
|
2f29ff7314 | ||
|
|
b37f043876 | ||
|
|
f0e725f65c | ||
|
|
23a34e2df1 | ||
|
|
d11e242b70 | ||
|
|
d9af0ca068 | ||
|
|
b7f10acbf0 | ||
|
|
43749ccdbd | ||
|
|
3bf4a8f8e6 | ||
|
|
f0ae1b3347 | ||
|
|
99809d25b9 | ||
|
|
e83c0af67c | ||
|
|
2ddf94313e | ||
|
|
da5965c956 | ||
|
|
31be0af3c9 | ||
|
|
38c550b245 | ||
|
|
95a7a2cef9 | ||
|
|
1a1ec51736 | ||
|
|
299aae56c1 | ||
|
|
781824c961 | ||
|
|
930cfa2590 | ||
|
|
35439f01aa | ||
|
|
71121e52af | ||
|
|
c08155717f | ||
|
|
de4164e7a4 | ||
|
|
e34d1242a9 | ||
|
|
5b82641018 | ||
|
|
de2f0c27b2 | ||
|
|
71e2afe781 | ||
|
|
3cba621fcf | ||
|
|
d79a2077c1 | ||
|
|
6925547b5f | ||
|
|
84aae8cf0a | ||
|
|
bdb42e39ec | ||
|
|
6edd54ee6d | ||
|
|
198f884d8b | ||
|
|
2c7d0a6721 | ||
|
|
7ef15ede0d | ||
|
|
3d96298b55 | ||
|
|
964f045e56 | ||
|
|
d3364ac109 | ||
|
|
a5b963c919 | ||
|
|
d6fcbbf543 | ||
|
|
f2e7e2eaf2 | ||
|
|
01c4c63114 | ||
|
|
22f9bc4ff1 | ||
|
|
c6c4350638 | ||
|
|
a17a0c4527 | ||
|
|
8e507012c1 | ||
|
|
beb4351dc9 | ||
|
|
afbb940721 | ||
|
|
f5c619a4c7 | ||
|
|
1b0401dff5 | ||
|
|
649e779100 | ||
|
|
f61e0e72a8 | ||
|
|
803f75fdde | ||
|
|
718d314eda | ||
|
|
adab6b0a6a | ||
|
|
d295ed2eca | ||
|
|
d18cbfa8cf | ||
|
|
59f881c4be | ||
|
|
0295e0ef63 | ||
|
|
dcdb95a055 | ||
|
|
e379d27722 | ||
|
|
41fbe0d2b7 | ||
|
|
1231666b06 | ||
|
|
b302bb8455 | ||
|
|
6e82405600 | ||
|
|
a678893bdb | ||
|
|
430348a3cd | ||
|
|
315401c166 | ||
|
|
b309c545f5 | ||
|
|
60b5d2d39b | ||
|
|
eb749a2a16 | ||
|
|
6db96001a3 | ||
|
|
257370ad58 | ||
|
|
557bf63b55 | ||
|
|
0e32e0a785 | ||
|
|
f2c607a4b2 | ||
|
|
0d12a9e118 | ||
|
|
02f418c8c5 | ||
|
|
ba618c9e4a | ||
|
|
ebd3ebe7b2 | ||
|
|
804117475b | ||
|
|
09ab8d6219 | ||
|
|
f3a5369690 | ||
|
|
8bf2fe624d | ||
|
|
50c1b667c5 | ||
|
|
360031d37c | ||
|
|
9ec873e0db | ||
|
|
c830a720b0 | ||
|
|
1aa7d1e0f7 | ||
|
|
c5c8de8628 | ||
|
|
74c6d6f5a1 | ||
|
|
2bff0faff7 | ||
|
|
4df028aa77 | ||
|
|
47c2d153aa | ||
|
|
18be09e9d5 | ||
|
|
55e940e88c | ||
|
|
e246f4e5ca | ||
|
|
5e1bb4b106 | ||
|
|
0b2889bb99 | ||
|
|
2994caf411 | ||
|
|
e157993a0f | ||
|
|
6c7c9afc34 | ||
|
|
2d0b17d93c | ||
|
|
033c613c89 | ||
|
|
1985eb59dd | ||
|
|
1cf6cbf8a3 | ||
|
|
0b42379c34 | ||
|
|
414c349974 | ||
|
|
cf6d5f7194 | ||
|
|
949f5207b4 | ||
|
|
a1da374b32 | ||
|
|
5460322d4a | ||
|
|
8b2da0b787 | ||
|
|
3eaf4dacaf | ||
|
|
d66d9b4dd7 | ||
|
|
cc52279e01 | ||
|
|
0def474f6d | ||
|
|
e0ea9a2855 | ||
|
|
2bc381fe05 | ||
|
|
fb3cd3bf52 | ||
|
|
c4414c7cc4 | ||
|
|
e2f2ceb7a9 | ||
|
|
641e7efb11 | ||
|
|
11f2ef50ef | ||
|
|
869ecfaf71 | ||
|
|
cb8731815c | ||
|
|
a9177ad362 | ||
|
|
ad116df73b | ||
|
|
f30b3a410c | ||
|
|
c440ba2d4b | ||
|
|
a3267dafdb | ||
|
|
7a1e83733c | ||
|
|
7cb96ce983 | ||
|
|
a73633d0c3 | ||
|
|
b2f8e8dd4e | ||
|
|
291d7abb78 | ||
|
|
32873d787b | ||
|
|
e243d71abf | ||
|
|
2689d3f21a | ||
|
|
c1bc008114 | ||
|
|
254578460a | ||
|
|
f5467dd3b9 | ||
|
|
9eb8714e11 | ||
|
|
847678ea56 | ||
|
|
f08729a402 | ||
|
|
a7c91257a7 | ||
|
|
835369a91e | ||
|
|
62554b522f | ||
|
|
fd041cd4c3 | ||
|
|
cfbb68c8ef | ||
|
|
d7acec4f7d | ||
|
|
7da46bca8b | ||
|
|
66b4fcdc2c | ||
|
|
c480579ca8 | ||
|
|
00ff1a1eae | ||
|
|
ae795f8ad3 | ||
|
|
9d3c071689 | ||
|
|
01cb23f566 | ||
|
|
fe8a9799ab | ||
|
|
4f18154681 | ||
|
|
2114c4a3ad | ||
|
|
2ca41b2b51 | ||
|
|
6605a2019e | ||
|
|
8b1ed5f183 | ||
|
|
f11a1b788f | ||
|
|
7928e24c54 | ||
|
|
5dbca41da6 | ||
|
|
f3fa54addf | ||
|
|
e636397f90 | ||
|
|
1f3e20704d | ||
|
|
cc9bdd4f14 | ||
|
|
6d76bf120d | ||
|
|
a50e81551f | ||
|
|
86512bcea1 | ||
|
|
975f45eb01 | ||
|
|
f2399d3179 | ||
|
|
b26d62a067 | ||
|
|
926d6cbd46 | ||
|
|
7ea47d2a99 | ||
|
|
89ad8cfc15 | ||
|
|
302a309aff | ||
|
|
fbc74c0012 | ||
|
|
27a953795c | ||
|
|
c3e62bc2e5 | ||
|
|
c2ab322bd2 | ||
|
|
aeab0f235c | ||
|
|
ae2285599f | ||
|
|
104ab757d2 | ||
|
|
6ada52bc0b | ||
|
|
c526cb9f08 | ||
|
|
a1662d76fb | ||
|
|
de84a8c8c5 | ||
|
|
4fa135daf0 | ||
|
|
9870cb4082 | ||
|
|
b2924761ab | ||
|
|
509e6bc2d8 |
@@ -55,7 +55,8 @@
|
||||
"block-spacing": "error",
|
||||
"keyword-spacing": "off",
|
||||
"space-before-blocks": "error",
|
||||
"semi-spacing": "error"
|
||||
"semi-spacing": "error",
|
||||
"no-constant-binary-expression": "error"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
230
CHANGELOG.md
230
CHANGELOG.md
@@ -6,6 +6,236 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v3.44.0] - 2023-01-06
|
||||
|
||||
- Add new `cartoon` visuals to support atomic nucleotide base with sugar
|
||||
- Add `thicknessFactor` to `cartoon` representation for scaling nucleotide block/ring/atomic-fill visuals
|
||||
- Use bonds from `_struct_conn` in mmCIF files that use `label_seq_id`
|
||||
- Fix measurement label `offsetZ` default: not needed when `scaleByRadius` is enbaled
|
||||
- Support for label rendering in HeadlessPluginContext
|
||||
- MolViewSpec extension
|
||||
- Support all X11 colors
|
||||
- Support relative URIs
|
||||
- CLI tools: mvs-validate, mvs-render, mvs-print-schema
|
||||
- Labels applied in one node
|
||||
- ModelServer SDF/MOL2 ligand export: fix atom indices when additional atoms are present
|
||||
- Avoid showing (and calculating) inter-unit bonds for huge structures
|
||||
- Fixed `DragOverlay` on WebKit/Safari browsers
|
||||
|
||||
## [v3.43.1] - 2023-12-04
|
||||
|
||||
- Fix `react-markdown` dependency
|
||||
|
||||
## [v3.43.0] - 2023-12-02
|
||||
|
||||
- Fix `State.tryGetCellData` (return type & data check)
|
||||
- Don't change camera.target unless flyMode or pointerLock are enabled
|
||||
- Handle empty CIF files
|
||||
- Snapshot improvements:
|
||||
- Add `key` property
|
||||
- Ability to existing snapshot name, key, and description
|
||||
- Support markdown in descriptions (ignores all HTML tags)
|
||||
- Ability to link to snapshots by key from descriptions
|
||||
- Separate UI control showing description of the current snapshot
|
||||
- Do not activate drag overlay for non-file content
|
||||
- Add `structure-element-sphere` visual to `spacefill` representation
|
||||
- Fix missing `await` in `HeadlessPluginContext.saveStateSnapshot`
|
||||
- Added support for providing custom sequence viewers to the plugin spec
|
||||
- MolViewSpec extension (MVS)
|
||||
- Add URL parameters `mvs-url`, `mvs-data`, `mvs-format`
|
||||
- Add drag&drop for `.mvsj` files
|
||||
- Fix `bumpiness` scaling with `ignoreLight` enabled
|
||||
- Add `transforms` & `label` params to `ShapeFromPly`
|
||||
- Optimize `LociSelectManager.selectOnly` to avoid superfluous loci set operations
|
||||
- Dispose of viewer on `unload` event to aid GC
|
||||
|
||||
## [v3.42.0] - 2023-11-05
|
||||
|
||||
- Fix handling of PDB files with insertion codes (#945)
|
||||
- Fix de-/saturate of colors with no hue
|
||||
- Improve `distinctColors` function
|
||||
- Add `sort` and `sampleCountFactor` parameters
|
||||
- Fix clustering issues
|
||||
- Add `clipPrimitive` option to spheres geometry, clipping whole spheres instead of cutting them
|
||||
- Add `DragAndDropManager`
|
||||
- Add `options` support for default bond labels
|
||||
|
||||
## [v3.41.0] - 2023-10-15
|
||||
|
||||
- Add `PluginContext.initialized` promise & support for it in the `Plugin` UI component.
|
||||
- Fix undesired interaction between settings panel and the panel on the right.
|
||||
- Add ability to customize server parameters for `RCSBAssemblySymmetry`.
|
||||
|
||||
## [v3.40.1] - 2023-09-30
|
||||
|
||||
- Do not call `updateFocusRepr` if default `StructureFocusRepresentation` isn't present.
|
||||
- Treat "tap" as a click in `InputObserver`
|
||||
- ModelServer ligand queries: fix atom count reported by SDF/MOL/MOL2 export
|
||||
- CCD extension: Make visuals for aromatic bonds configurable
|
||||
- Add optional `file?: CifFile` to `MmcifFormat.data`
|
||||
- Add support for webgl extensions
|
||||
- `WEBGL_clip_cull_distance`
|
||||
- `EXT_conservative_depth`
|
||||
- `WEBGL_stencil_texturing`
|
||||
- `EXT_clip_control`
|
||||
- Add `MultiSampleParams.reduceFlicker` (to be able to switch it off)
|
||||
- Add `alphaThickness` parameter to adjust alpha of spheres for radius
|
||||
- Ability to hide "right" panel from simplified viewport controls
|
||||
- Add `blockIndex` parameter to TrajectoryFromMmCif
|
||||
- Fix bounding sphere calculation for "element-like" visuals
|
||||
- Fix RCSB PDB validation report URL
|
||||
- Add sharpening postprocessing option
|
||||
- Take pixel-ratio into account for outline scale
|
||||
- Gracefully handle missing HTMLImageElement
|
||||
- Fix pixel-ratio changes not applied to all render passes
|
||||
|
||||
## [v3.39.0] - 2023-09-02
|
||||
|
||||
- Add some elements support for `guessElementSymbolString` function
|
||||
- Faster bounding rectangle calculation for imposter spheres
|
||||
- Allow toggling of hydrogens as part of `LabelTextVisual`
|
||||
|
||||
## [v3.38.3] - 2023-07-29
|
||||
|
||||
- Fix imposter spheres not updating, e.g. in trajectories (broke in v3.38.0)
|
||||
|
||||
## [v3.38.2] - 2023-07-24
|
||||
|
||||
- Don't rely solely on `chem_comp_atom` when detecting CCD files (#877)
|
||||
- Actually support non-physical keys in `Bindings.Trigger.code`
|
||||
|
||||
## [v3.38.1] - 2023-07-22
|
||||
|
||||
- Fix pixel-scale not updated in SSAO pass
|
||||
|
||||
## [v3.38.0] - 2023-07-18
|
||||
|
||||
- Fix display issue with SIFTS mapping
|
||||
- Support non-physical keys in `Bindings.Trigger.code`
|
||||
- Update `getStateSnapshot` to only overwrite current snapshot if it was created automatically
|
||||
- Fix distinct palette's `getSamples` infinite loop
|
||||
- Add 'NH2', 'FOR', 'FMT' to `CommonProteinCaps`
|
||||
- Add `opened` event to `PluginStateSnapshotManager`
|
||||
- Properly switch-off fog
|
||||
- Add `approximate` option for spheres rendering
|
||||
- Reduce `Spheres` memory usage
|
||||
- Derive mapping from VertexID
|
||||
- Pull position and group from texture
|
||||
- Add `Euler` math primitive
|
||||
- Add stride option to element sphere & point visuals
|
||||
- Add `disabledExtensions` field to default viewer's options
|
||||
- Add `LRUCache.remove`
|
||||
- Add 'Chain Instance' and 'Uniform' options for 'Carbon Color' param (in Color Theme: Element Symbol)
|
||||
|
||||
## [v3.37.1] - 2023-06-20
|
||||
|
||||
- Fix issues with wboit/dpoit in large scenes
|
||||
- Fix lines, text, points rendering (broken in v3.37.0)
|
||||
|
||||
## [v3.37.0] - 2023-06-17
|
||||
|
||||
- Add `inverted` option to `xrayShaded` parameter
|
||||
- Model-export extension: Add ability to set a file name for structures
|
||||
- Add `contextHash` to `SizeTheme`
|
||||
- Add mipmap-based blur for image backgrounds
|
||||
|
||||
## [v3.36.1] - 2023-06-11
|
||||
|
||||
- Allow parsing of CCD ligand files
|
||||
- Add dedicated wwPDB CCD extension to align and visualize ideal & model CCD coordinates
|
||||
- Make operators in `IndexPairBonds` a directed property
|
||||
- Remove erroneous bounding-box overlap test in `Structure.eachUnitPair`
|
||||
- Fix `EdgeBuilder.addNextEdge` for loop edges
|
||||
- Optimize inter unit bond compute
|
||||
- Ensure consistent state for volume representation (#210)
|
||||
- Improve SSAO for thin geometry (e.g. lines)
|
||||
- Add snapshot support for structure selections
|
||||
- Add `nucleicProfile` parameter to cartoon representation
|
||||
- Add `cartoon` theme with separate colorings for for mainchain and sidechain visuals
|
||||
|
||||
## [v3.35.0] - 2023-05-14
|
||||
|
||||
- Enable odd dash count (1,3,5)
|
||||
- Add principal axes spec and fix edge cases
|
||||
- Add a uniform color theme for NtC tube that still paints residue and segment dividers in a different color
|
||||
- Mesh exporter improvements
|
||||
- Support points & lines in glTF export
|
||||
- Set alphaMode and doubleSided in glTF export
|
||||
- Fix flipped cylinder caps
|
||||
- Fix bond assignments `struct_conn` records referencing waters
|
||||
- Add StructConn extension providing functions for inspecting struct_conns
|
||||
- Fix `PluginState.setSnapshot` triggering unnecessary state updates
|
||||
- Fix an edge case in the `mol-state`'s `State` when trying to apply a transform to an existing Null object
|
||||
- Add `SbNcbrPartialCharges` extension for coloring and labeling atoms and residues by partial atomic charges
|
||||
- uses custom mmcif categories `_sb_ncbr_partial_atomic_charges_meta` and `_sb_ncbr_partial_atomic_charges` (more info in [README.md](./src/extensions/sb-ncbr/README.md))
|
||||
- Parse HEADER record when reading PDB file
|
||||
- Support `ignoreHydrogens` in interactions representation
|
||||
- Add hydroxyproline (HYP) commonly present in collagen molecules to the list of amino acids
|
||||
- Fix assemblies for Archive PDB files (do not generate unique `label_asym_id` if `REMARK 350` is present)
|
||||
- Add additional functions to `core.math` in `mol-script`
|
||||
- `cantorPairing`, `sortedCantorPairing`, `invertCantorPairing`,
|
||||
- `trunc`, `sign`
|
||||
|
||||
## [v3.34.0] - 2023-04-16
|
||||
|
||||
- Avoid `renderMarkingDepth` for fully transparent renderables
|
||||
- Remove `camera.far` doubling workaround
|
||||
- Add `ModifiersKeys.areNone` helper function
|
||||
- Do not render NtC tube segments unless all required atoms are present in the structure
|
||||
- Fix rendering issues caused by VAO reuse
|
||||
- Add "Zoom All", "Orient Axes", "Reset Axes" buttons to the "Reset Camera" button
|
||||
- Improve trackball move-state handling when key bindings use modifiers
|
||||
- Fix rendering with very small viewport and SSAO enabled
|
||||
- Fix `.getAllLoci` for structure representations with `structure.child`
|
||||
- Fix `readAllLinesAsync` refering to dom length property
|
||||
- Make mol-util/file-info node compatible
|
||||
- Add `eachLocation` to representation/visual interface
|
||||
|
||||
## [v3.33.0] - 2023-04-02
|
||||
|
||||
- Handle resizes of viewer element even when window remains the same size
|
||||
- Throttle canvas resize events
|
||||
- Selection toggle buttons hidden if selection mode is off
|
||||
- Camera focus loci bindings allow reset on click-away to be overridden
|
||||
- Input/controls improvements
|
||||
- Move or fly around the scene using keys
|
||||
- Pointer lock to look around scene
|
||||
- Toggle spin/rock animation using keys
|
||||
- Apply bumpiness as lightness variation with `ignoreLight`
|
||||
- Remove `JSX` reference from `loci-labels.ts`
|
||||
- Fix overpaint/transparency/substance smoothing not updated when geometry changes
|
||||
- Fix camera project/unproject when using offset viewport
|
||||
- Add support for loading all blocks from a mmcif file as a trajectory
|
||||
- Add `Frustum3D` and `Plane3D` math primitives
|
||||
- Include `occupancy` and `B_iso_or_equiv` when creating `Conformation` from `Model`
|
||||
- Remove LazyImports (introduced in v3.31.1)
|
||||
|
||||
## [v3.32.0] - 2023-03-20
|
||||
|
||||
- Avoid rendering of fully transparent renderables
|
||||
- Add occlusion color parameter
|
||||
- Fix issue with outlines and orthographic camera
|
||||
- Reduce over-blurring occlusion at larger view distances
|
||||
- Fix occlusion artefact with non-canvas viewport and pixel-ratio > 1
|
||||
- Update nodejs-shims conditionals to handle polyfilled document object in NodeJS environment.
|
||||
- Ensure marking edges are at least one pixel wide
|
||||
- Add exposure parameter to renderer
|
||||
- Only trigger marking when mouse is directly over canvas
|
||||
- Fix blurry occlusion in screenshots
|
||||
- [Breaking] Add `setFSModule` to `mol-util/data-source` instead of trying to trick WebPack
|
||||
|
||||
## [v3.31.4] - 2023-02-24
|
||||
|
||||
- Allow link cylinder/line `dashCount` set to '0'
|
||||
- Stop animation loop when disposing `PluginContext` (thanks @gfrn for identifying the issue)
|
||||
|
||||
## [v3.31.3] - 2023-02-22
|
||||
|
||||
- Fix impostor bond visuals not correctly updating on `sizeFactor` changes
|
||||
- Fix degenerate case in PCA
|
||||
- Fix near clipping avoidance in impostor shaders
|
||||
- Update `fs` import in `data-source.ts`
|
||||
|
||||
## [v3.31.2] - 2023-02-12
|
||||
|
||||
- Fix exit code of volume pack executable (pack.ts). Now exits with non-0 status when an error happens
|
||||
|
||||
BIN
docs/extensions/mvs/1cbs.png
Normal file
BIN
docs/extensions/mvs/1cbs.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
161
docs/extensions/mvs/README.md
Normal file
161
docs/extensions/mvs/README.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# Mol* MolViewSpec extension
|
||||
|
||||
**MolViewSpec (MVS)** is a tool for standardized description of reproducible molecular visualizations shareable across software applications.
|
||||
|
||||
MolViewSpec provides a generic description of typical visual scenes that may occur as part of molecular visualizations. A tree format allows the composition of complex scene descriptors by combining reoccurring nodes that serve as building blocks.
|
||||
|
||||
|
||||
## More sources:
|
||||
|
||||
- MolViewSpec home page: https://molstar.org/mol-view-spec/
|
||||
- Python library `molviewspec` for building MolViewSpec views: https://pypi.org/project/molviewspec/
|
||||
- Python library `molviewspec` in action: https://colab.research.google.com/drive/1O2TldXlS01s-YgkD9gy87vWsfCBTYuz9
|
||||
|
||||
|
||||
## MolViewSpec data structure
|
||||
|
||||
MVS is based on a tree format, i.e. a molecular view is described as a tree where individual node types represent common data operations needed to create the view (e.g. download, parse, color). Each node can have parameters that provide additional details for the operation.
|
||||
|
||||
A simple example of a MVS tree showing PDB structure 1cbs:
|
||||
|
||||

|
||||
|
||||
```txt
|
||||
- root {}
|
||||
- download {url: "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"}
|
||||
- parse {format: "bcif"}
|
||||
- structure {type: "model"}
|
||||
- component {selector: "polymer"}
|
||||
- representation {type: "cartoon"}
|
||||
- color {color: "green"}
|
||||
- color {selector: {label_asym_id: "A", beg_label_seq_id: 1, end_label_seq_id: 50}, color: "#6688ff"}
|
||||
- label {text: "Protein"}
|
||||
- component {selector: "ligand"}
|
||||
- representation {type: "ball_and_stick"}
|
||||
- color {color: "#cc3399"}
|
||||
- label {text: "Retinoic Acid"}
|
||||
- canvas {background_color: "#ffffee"}
|
||||
- camera {target: [17,21,27], position: [41,34,69], up: [-0.129,0.966,-0.224]}
|
||||
```
|
||||
|
||||
(This is just a human-friendly representation of the tree, not the actual data format!)
|
||||
|
||||
A complete list of supported node types and their parameters is described by the [MVS tree schema](./mvs-tree-schema.md).
|
||||
|
||||
### Encoding
|
||||
|
||||
A MolViewSpec tree can be encoded and stored in `.mvsj` format, which is basically a JSON representation of the tree with additional metadata:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Example MolViewSpec - 1cbs with labelled protein and ligand",
|
||||
"version": "1",
|
||||
"timestamp": "2023-11-24T10:38:17.483Z"
|
||||
},
|
||||
"root": {
|
||||
"kind": "root",
|
||||
"children": [
|
||||
{
|
||||
"kind": "download",
|
||||
"params": {"url": "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"},
|
||||
"children": [
|
||||
{
|
||||
"kind": "parse",
|
||||
"params": {"format": "bcif"},
|
||||
"children": [
|
||||
...
|
||||
```
|
||||
Complete file: [1cbs.mvsj](../../../examples/mvs/1cbs.mvsj)
|
||||
|
||||
|
||||
## MolViewSpec extension functionality
|
||||
|
||||
Mol* MolViewSpec extension provides functionality for building, validating, and visualizing MVS views.
|
||||
|
||||
### Graphical user interface
|
||||
|
||||
- **Drag&drop support:** The easiest way to load a MVS view into Mol* Viewer is to drag a `.mvsj` file and drop it in a browser window with Mol* Viewer.
|
||||
|
||||
- **Load via menu:** Another way to load a MVS view is to use "Download File" or "Open Files" action, available in the "Home" tab in the left panel. For these actions, the "Format" parameter must be set to "MVSJ" (in the "Miscellaneous" category) or "Auto".
|
||||
|
||||
- **URL parameters:** Mol* Viewer supports `mvs-url`, `mvs-data`, and `mvs-format` URL parameters to specify a MVS view to be loaded when the viewer is initialized.
|
||||
- `mvs-url` specifies the address from which the MVS view should be retrieved.
|
||||
- `mvs-data` specifies the MVS view data directly. Keep in mind that some characters must be escaped to be used in the URL. Also beware that URLs longer than 2000 character may not work in all browsers.
|
||||
- `mvs-format` specifies the format of the MVS view data (from `mvs-url` or `mvs-data`). The only allowed (and default) value is `mvsj`, as this is currently the only supported format.
|
||||
|
||||
Examples of URL parameter usage:
|
||||
|
||||
- https://molstar.org/viewer?mvs-format=mvsj&mvs-url=https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj
|
||||
|
||||
- https://molstar.org/viewer?mvs-format=mvsj&mvs-data=%7B%22metadata%22%3A%7B%22title%22%3A%22Example%20MolViewSpec%20-%201cbs%20with%20labelled%20protein%20and%20ligand%22%2C%22version%22%3A%221%22%2C%22timestamp%22%3A%222023-11-24T10%3A38%3A17.483%22%7D%2C%22root%22%3A%7B%22kind%22%3A%22root%22%2C%22children%22%3A%5B%7B%22kind%22%3A%22download%22%2C%22params%22%3A%7B%22url%22%3A%22https%3A//www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22parse%22%2C%22params%22%3A%7B%22format%22%3A%22bcif%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22structure%22%2C%22params%22%3A%7B%22type%22%3A%22model%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22polymer%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22cartoon%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22green%22%7D%7D%2C%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22selector%22%3A%7B%22label_asym_id%22%3A%22A%22%2C%22beg_label_seq_id%22%3A1%2C%22end_label_seq_id%22%3A50%7D%2C%22color%22%3A%22%236688ff%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Protein%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22component%22%2C%22params%22%3A%7B%22selector%22%3A%22ligand%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22representation%22%2C%22params%22%3A%7B%22type%22%3A%22ball_and_stick%22%7D%2C%22children%22%3A%5B%7B%22kind%22%3A%22color%22%2C%22params%22%3A%7B%22color%22%3A%22%23cc3399%22%7D%7D%5D%7D%2C%7B%22kind%22%3A%22label%22%2C%22params%22%3A%7B%22text%22%3A%22Retinoic%20Acid%22%7D%7D%5D%7D%5D%7D%5D%7D%5D%7D%2C%7B%22kind%22%3A%22canvas%22%2C%22params%22%3A%7B%22background_color%22%3A%22%23ffffee%22%7D%7D%2C%7B%22kind%22%3A%22camera%22%2C%22params%22%3A%7B%22target%22%3A%5B17%2C21%2C27%5D%2C%22position%22%3A%5B41%2C34%2C69%5D%2C%22up%22%3A%5B-0.129%2C0.966%2C-0.224%5D%7D%7D%5D%7D%7D
|
||||
|
||||
|
||||
### Programming interface
|
||||
|
||||
Most functions for manipulation of MVS data (including parsing, encoding, validating, and building) are provided by the `MVSData` object (defined in [src/extensions/mvs/mvs-data.ts](/src/extensions/mvs/mvs-data.ts)). In TypeScript, `MVSData` is also the type for a MVS view.
|
||||
|
||||
The `loadMVS` function (defined in [src/extensions/mvs/load.ts](/src/extensions/mvs/load.ts)) can be used to load MVS view data into Mol* Viewer.
|
||||
|
||||
Example usage:
|
||||
|
||||
```ts
|
||||
// Fetch a MVS, validate, and load
|
||||
const response = await fetch('https://raw.githubusercontent.com/molstar/molstar/master/examples/mvs/1cbs.mvsj');
|
||||
const rawData = await response.text();
|
||||
const mvsData: MVSData = MVSData.fromMVSJ(rawData);
|
||||
if (!MVSData.isValid(mvsData)) throw new Error(`Oh no: ${MVSData.validationIssues(mvsData)}`);
|
||||
await loadMVS(this.plugin, mvsData, { replaceExisting: true });
|
||||
console.log('Loaded this:', MVSData.toPrettyString(mvsData));
|
||||
console.log('Loaded this:', MVSData.toMVSJ(mvsData));
|
||||
|
||||
// Build a MVS and load
|
||||
const builder = MVSData.createBuilder();
|
||||
const structure = builder
|
||||
.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.modelStructure();
|
||||
structure
|
||||
.component({ selector: 'polymer' })
|
||||
.representation({ type: 'cartoon' });
|
||||
structure
|
||||
.component({ selector: 'ligand' })
|
||||
.representation({ type: 'ball_and_stick' })
|
||||
.color({ color: '#aa55ff' });
|
||||
const mvsData2: MVSData = builder.getState();
|
||||
await loadMVS(this.plugin, mvsData2, { replaceExisting: false });
|
||||
```
|
||||
|
||||
When using the pre-built Mol* plugin bundle, `MVSData` and `loadMVS` are exposed as `molstar.PluginExtensions.mvs.MVSData` and `molstar.PluginExtensions.mvs.loadMVS`. Furthermore, the `molstar.Viewer` class has `loadMvsFromUrl` and `loadMvsData` methods, providing the same functionality as `mvs-url` and `mvs-data` URL parameters.
|
||||
|
||||
|
||||
### Command-line utilities
|
||||
|
||||
The MVS extension in Mol* provides a few command-line utilities, which can be executed via NodeJS:
|
||||
|
||||
- `mvs-validate` provides validation of MolViewSpec files
|
||||
- `mvs-render` creates images based on MolViewSpec files
|
||||
- `mvs-print-schema` prints MolViewSpec tree schema (i.e. currently supported node types and their parameters)
|
||||
|
||||
Example usage:
|
||||
|
||||
```sh
|
||||
# Validate a MolViewSpec file `examples/mvs/1cbs.mvsj`
|
||||
node lib/commonjs/cli/mvs/mvs-validate examples/mvs/1cbs.mvsj
|
||||
|
||||
# Render a MolViewSpec file `examples/mvs/1cbs.mvsj` to `../outputs/1cbs.png`
|
||||
npm install --no-save canvas gl jpeg-js pngjs # Might be needed before the first execution
|
||||
node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
|
||||
|
||||
# Print MolViewSpec tree schema formatted as markdown
|
||||
node lib/commonjs/cli/mvs/mvs-print-schema --markdown
|
||||
```
|
||||
|
||||
(If you installed Mol* package from the npm repository, use can just type `npx mvs-validate`...).
|
||||
|
||||
|
||||
## Topics
|
||||
|
||||
- [Selectors](./selectors.md)
|
||||
- [Annotations](./annotations.md)
|
||||
- [Camera Settings](./camera-settings.md)
|
||||
185
docs/extensions/mvs/annotations.md
Normal file
185
docs/extensions/mvs/annotations.md
Normal file
@@ -0,0 +1,185 @@
|
||||
# MVS annotations
|
||||
|
||||
Annotations are used to define substructures (components) and apply colors, labels, or tooltips to them. In contrast to [selectors](./selectors.md), annotations are defined in a separate file, which can then be referenced in the main MVS file.
|
||||
|
||||
|
||||
## MVS annotation files
|
||||
|
||||
MVS annotations can be encoded in multiple different formats, but their logic is always the same and in fact very similar to that of selectors.
|
||||
|
||||
### JSON format
|
||||
|
||||
The simplest example of an annotation in JSON format is just a JSON-encoded [union component expression](./selectors.md) selector. Here is a simple annotation containing 4 **annotation rows**:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "label_asym_id": "A" },
|
||||
{ "label_asym_id": "B" },
|
||||
{ "label_asym_id": "B", "beg_label_seq_id": 100, "end_label_seq_id": 200 },
|
||||
{ "label_asym_id": "B", "beg_label_seq_id": 150, "end_label_seq_id": 160 },
|
||||
]
|
||||
```
|
||||
|
||||
However, in a typical annotation, there is at least one extra field that provides the value of the dependent variable (such as color or label) mapped to each annotation row:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "label_asym_id": "A", "color": "#00ff00" },
|
||||
{ "label_asym_id": "B", "color": "blue" },
|
||||
{ "label_asym_id": "B", "beg_label_seq_id": 100, "end_label_seq_id": 200, "color": "skyblue" }
|
||||
{ "label_asym_id": "B", "beg_label_seq_id": 150, "end_label_seq_id": 160, "color": "lightblue" }
|
||||
]
|
||||
```
|
||||
|
||||
This particular annotation (when applied via `color_from_uri` node) will apply green color (#00ff00) to the whole chain A and three shades of blue to the chain B. Later annotation rows override earlier rows, therefore residues 1–99 will be blue, 100–149 skyblue, 150–160 lightblue, 161–200 skyblue, and 201–end blue. (Tip: to color all the rest of the structure in one color, add an annotation row with no selector fields (e.g. `{ "color": "yellow" }`) to the beginning of the annotation.)
|
||||
|
||||
Real-life annotation files can include huge numbers of annotation rows. To avoid repeating the same field keys in every row, we can convert the array-of-objects into object-of-arrays. This will result in an equivalent annotation but smaller file size:
|
||||
|
||||
```json
|
||||
{
|
||||
"label_asym_id": ["A", "B", "B", "B"],
|
||||
"beg_label_seq_id": [null, null, 100, 150],
|
||||
"end_label_seq_id": [null, null, 200, 160],
|
||||
"color": ["#00ff00", "blue", "skyblue", "lightblue"]
|
||||
}
|
||||
```
|
||||
|
||||
A more complex example of JSON annotation is provided in [/examples/mvs/1h9t_domains.json](/examples/mvs/1h9t_domains.json).
|
||||
|
||||
### CIF format
|
||||
|
||||
Annotations can also be encoded using CIF format, a table-based format which is commonly used in structure biology to store structures or any kind of tabular data.
|
||||
|
||||
The example from above, encoded as CIF, would look like this:
|
||||
|
||||
```cif
|
||||
data_annotation
|
||||
loop_
|
||||
_coloring.label_asym_id
|
||||
_coloring.beg_label_seq_id
|
||||
_coloring.end_label_seq_id
|
||||
_coloring.color
|
||||
A . . '#00ff00'
|
||||
B . . 'blue'
|
||||
B 100 200 'skyblue'
|
||||
B 150 160 'lightblue'
|
||||
```
|
||||
|
||||
An advantage of the CIF format is that it can include multiple annotation tables in the same file, organized into blocks and categories. Then the MVS file can reference individual tables using `block_header` (or `block_index`) and `category_name` parameters. The column containing the dependent variable can be specified using `field_name` parameter. In this case, we could use `"block_header": "annotation", "category_name": "coloring", "field_name": "color"`.
|
||||
|
||||
### BCIF format
|
||||
|
||||
This has exactly the same structure as the CIF format, but encoded using [BinaryCIF](https://github.com/molstar/BinaryCIF).
|
||||
|
||||
|
||||
## Referencing MVS annotations in MVS tree
|
||||
|
||||
### From URI
|
||||
|
||||
MVS annotations can be referenced in `color_from_uri`, `label_from_uri`, `tooltip_from_uri`, and `component_from_uri` nodes in MVS tree.
|
||||
|
||||
For example this part of a MVS tree:
|
||||
|
||||
```txt
|
||||
- representation {type: "cartoon"}
|
||||
- color {selector: {label_asym_id: "A"}, color: "#00ff00"}
|
||||
- color {selector: {label_asym_id: "B"}, color: "blue"}
|
||||
- color {selector: {label_asym_id: "B", beg_label_seq_id: 100, end_label_seq_id: 200}, color: "skyblue"}
|
||||
- color {selector: {label_asym_id: "B", beg_label_seq_id: 150, end_label_seq_id: 160}, color: "lightblue"}
|
||||
```
|
||||
|
||||
can be replaced by:
|
||||
|
||||
```txt
|
||||
- representation {type: "cartoon"}
|
||||
- color_from_uri {uri: "https://example.org/annotations.json", format: "json", schema: "residue_range"}
|
||||
```
|
||||
|
||||
assuming that the JSON annotation file shown in the previous section is available at `https://example.org/annotations.json`.
|
||||
|
||||
#### Relative URIs
|
||||
|
||||
The `uri` parameter can also hold a URI reference (relative URI). In such cases, this URI reference is relative to the URI of the MVS file itself (e.g. if the MVS file is available from `https://example.org/spanish/inquisition/expectations.mvsj`, then the relative URI `./annotations.json` is equivalent to `https://example.org/spanish/inquisition/annotations.json`). This is however not applicable in all cases (e.g. the MVS tree can be constructed ad-hoc within a web application, therefore it has no URI; or the MVS file is loaded from a local disk using drag&drop, therefore the relative location is not accessible by the browser).
|
||||
|
||||
### From source
|
||||
|
||||
The MVS annotations can in fact be stored within the same mmCIF file from which the structure coordinates are loaded. To reference these annotations, we can use `color_from_source`, `label_from_source`, `tooltip_from_source`, and `component_from_source` nodes. Example:
|
||||
|
||||
```txt
|
||||
- representation {type: "cartoon"}
|
||||
- color_from_source {schema: "residue_range", block_header: "annotation", category_name: "coloring"}
|
||||
```
|
||||
|
||||
|
||||
## Annotation schemas
|
||||
|
||||
The `schema` parameter of all `*_from_uri` and `*_from_source` nodes specifies the MVS annotation schema, i.e. a set of fields used to select a substructure. In the example above we are using `residue_range` schema, which uses columns `label_entity_id`, `label_asym_id`, `beg_label_seq_id`, and `end_label_seq_id`. (We didn't provide values for `label_entity_id`, so it is not taken into account even though the schema supports it).
|
||||
|
||||
|
||||
Table of selector field names supported by individual MVS annotation schemas:
|
||||
|
||||
|Field \ Schema|whole_structure|entity|chain|residue|residue_range|atom|auth_chain|auth_residue|auth_residue_range|auth_atom|all_atomic|
|
||||
|:------------------|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|:-:|
|
||||
| label_entity_id | | X | X | X | X | X | | | | | X |
|
||||
| label_asym_id | | | X | X | X | X | | | | | X |
|
||||
| label_seq_id | | | | X | | X | | | | | X |
|
||||
| beg_label_seq_id | | | | | X | | | | | | X |
|
||||
| end_label_seq_id | | | | | X | | | | | | X |
|
||||
| label_atom_id | | | | | | X | | | | | X |
|
||||
| auth_asym_id | | | | | | | X | X | X | X | X |
|
||||
| auth_seq_id | | | | | | | | X | | X | X |
|
||||
| pdbx_PDB_ins_code | | | | | | | | X | | X | X |
|
||||
| beg_auth_seq_id | | | | | | | | | X | | X |
|
||||
| end_auth_seq_id | | | | | | | | | X | | X |
|
||||
| auth_atom_id | | | | | | | | | | X | X |
|
||||
| type_symbol | | | | | | X | | | | X | X |
|
||||
| atom_id | | | | | | X | | | | X | X |
|
||||
| atom_index | | | | | | X | | | | X | X |
|
||||
|
||||
To include all selector field names that are present in the annotation, one can use `"schema": "all_atomic"` (we could use it in the example above and the result would be the same). In future versions of MVS, non-atomic schemas might be added, to select parts of structures that are not composed of atoms, e.g. coarse models or geometric primitives.
|
||||
|
||||
|
||||
## `group_id` field
|
||||
|
||||
The `group_id` field is a special field supported by all MVS annotation schemas. It does not change the sets of atoms selected by individual rows but instead groups annotation rows together to create more complex selections. This is useful when adding labels to our visualization.
|
||||
|
||||
The following example (when applied via `label_from_uri` node) will create 7 separate labels, each bound to a single residue:
|
||||
|
||||
```cif
|
||||
data_annotation
|
||||
loop_
|
||||
_labels.label_asym_id
|
||||
_labels.label_seq_id
|
||||
_labels.color
|
||||
_labels.label
|
||||
A 100 pink 'Substrate binding site'
|
||||
A 150 pink 'Substrate binding site'
|
||||
A 170 pink 'Substrate binding site'
|
||||
A 200 blue 'Inhibitor binding site'
|
||||
A 220 blue 'Inhibitor binding site'
|
||||
A 300 lime 'Glycosylation site'
|
||||
A 330 lime 'Glycosylation site'
|
||||
```
|
||||
|
||||
On the other hand, the next example will only create 4 labels ("Substrate binding site" label bound to residues 100, 150, and 170; "Inhibitor binding site" label bound to residues 200 and 220; "Glycosylation site" label bound to residue 300; and "Glycosylation site" label bound to residue 330):
|
||||
|
||||
```cif
|
||||
data_annotation
|
||||
loop_
|
||||
_labels.group_id
|
||||
_labels.label_asym_id
|
||||
_labels.label_seq_id
|
||||
_labels.color
|
||||
_labels.label
|
||||
1 A 100 pink 'Substrate binding site'
|
||||
1 A 150 pink 'Substrate binding site'
|
||||
1 A 170 pink 'Substrate binding site'
|
||||
2 A 200 blue 'Inhibitor binding site'
|
||||
2 A 220 blue 'Inhibitor binding site'
|
||||
. A 300 lime 'Glycosylation site'
|
||||
. A 330 lime 'Glycosylation site'
|
||||
```
|
||||
|
||||
Note: Annotation rows with empty `group_id` field (`.` in CIF, ommitted field or `null` in JSON) are always treated as separate groups.
|
||||
|
||||
Note 2: `group_id` field has no effect on colors, tooltips, components. It only makes any difference for labels.
|
||||
71
docs/extensions/mvs/camera-settings.md
Normal file
71
docs/extensions/mvs/camera-settings.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# MVS camera settings
|
||||
|
||||
Camera position and orientation in MVS views can be adjusted in two ways: using a `camera` node or a `focus` node. Global attributes of the MVS view unrelated to camera positioning can be adjusted via a `canvas` node.
|
||||
|
||||
## `camera` node
|
||||
|
||||
This node instructs to directly set the camera position and orientation. This is done by passing `target`, `position`, and optional `up` vector. The `camera` node is placed as a child of the `root` node (see [MVS tree schema](./mvs-tree-schema.md#camera)).
|
||||
|
||||
However, if the `target` and `position` vectors were interpreted directly, the resulting view would wildly depend on the camera field of view (FOV). For example, assume we have a sphere with center in the point [0,0,0] and radius 10 Angstroms, and we set `target=[0,0,0]` and `position=[0,0,20]`. With a camera with vertical FOV=90°, the sphere will fit into the camera's view nicely, with some margin above and under the sphere. But with a camera with vertical FOV=30°, the top and bottom of sphere will be cropped. To avoid these differences, MVS always uses position of a "reference camera" instead of the real camera position.
|
||||
|
||||
We define the "reference camera" as a camera with such FOV that a sphere with radius *R* viewed from distance 2*R* (from the center of the sphere) will just fit into view (i.e. there will be no margin but the sphere will not be cropped). This happens to be FOV = 2 arcsin(1/2) = 60° for perspective projection, and FOV = 2 arctan(1/2) ≈ 53° for orthographic projection.
|
||||
|
||||
|
||||
When using **perspective** projection, the real camera distance from target and the real camera position can be calculated using these formulas:
|
||||
|
||||
$d _\mathrm{adj} = d _\mathrm{ref} \cdot \frac{1}{2 \sin(\alpha/2)}$
|
||||
|
||||
$\mathbf{p} _\mathrm{adj} = \mathbf{t} + (\mathbf{p} _\mathrm{ref} - \mathbf{t}) \cdot \frac{1}{2 \sin(\alpha/2)}$
|
||||
|
||||
Where $\alpha$ is the vertical FOV of the real camera, $d _\mathrm{ref}$ is the reference camera distance from target, $d _\mathrm{adj}$ is the real (adjusted) camera distance from target, $\mathbf{t}$ is the target position, $\mathbf{p} _\mathrm{ref}$ is the reference camera position (the actual value in the MVS file), and $\mathbf{p} _\mathrm{adj}$ is the real (adjusted) camera position.
|
||||
|
||||
When using **orthographic** projection, the formulas are slightly different:
|
||||
|
||||
$d _\mathrm{adj} = d _\mathrm{ref} \cdot \frac{1}{2 \tan(\alpha/2)}$
|
||||
|
||||
$\mathbf{p} _\mathrm{adj} = \mathbf{t} + (\mathbf{p} _\mathrm{ref} - \mathbf{t}) \cdot \frac{1}{2 \tan(\alpha/2)}$
|
||||
|
||||
|
||||
Using the example above (`target=[0,0,0]` and `position=[0,0,20]`), we can calculate that the real camera position will have to be set to:
|
||||
|
||||
- [0, 0, 14.14] for FOV=90° (perspective projection)
|
||||
- [0, 0, 20] for FOV=60° (perspective projection)
|
||||
- [0, 0, 38.68] for FOV=30° (perspective projection)
|
||||
|
||||
Note that for orthographic projection this adjustment achieves that the resulting view does not depend on the FOV value. For perspective projection, this is not possible and there will always be some "fisheye effect", but still it greatly reduces the dependence on FOV and avoids the too-much-zoomed-in and too-much-zoomed-out views when FOV changes.
|
||||
|
||||
|
||||
The `up` vector describes how the camera should be rotated around the position-target axis, i.e. it is the vector in 3D space that will be point up when projected on the screen. For this, the `up` vector must be perpendicular to the position-target axis. However, the MVS specification does not require that the provided `up` vector be perpendicular. This can be solved by a simple adjustment:
|
||||
|
||||
$\mathbf{u} _\mathrm{adj} = \mathrm{normalize} ( ((\mathbf{t}-\mathbf{p}) \times \mathbf{u}) \times (\mathbf{t}-\mathbf{p}) )$
|
||||
|
||||
Where $\mathbf{u}$ is the unadjusted up vector (the actual value in the MVS file), $\mathbf{u} _\mathrm{adj}$ is the adjusted up vector, $\mathbf{t}$ is the target position, and $\mathbf{p}$ is the camera position (can be either reference or adjusted camera position, the result will be the same).
|
||||
|
||||
If the up vector parameter is not provided, the default value ([0, 1, 0]) will be used (after adjustment).
|
||||
|
||||
|
||||
## `focus` node
|
||||
|
||||
The other way to adjust camera is to use a `focus` node. This node is placed as a child of a `component` node and instructs to set focus to the parent component (zoom in). This means that the camera target should be set to the center of the bounding sphere of the component, and the camera position should be set so that the bounding sphere just fits into view (vertically and horizontally).
|
||||
|
||||
By default, the camera will be oriented so that the X axis points right, the Y axis points up, and the Z axis points towards the observer. This orientation can be changed using the optional vector parameters `direction` and `up` (see [MVS tree schema](./mvs-tree-schema.md#focus)). The `direction` vector describes the direction from the camera position towards the target position (default [0, 0, -1]). The meaning of the `up` vector is the same as for the `camera` node and the same adjustment applies to it (default [0, 1, 0]).
|
||||
|
||||
|
||||
|
||||
The reference camera position for a `focus` node can be calculated as follows:
|
||||
|
||||
$\mathbf{p} _\mathrm{ref} = \mathbf{t} - \mathrm{normalize}(\mathbf{d}) \cdot 2 r \cdot \max(1, \frac{h}{w})$
|
||||
|
||||
Where $\mathbf{t}$ is the target position (center of the bounding sphere of the component), $r$ is the radius of the bounding sphere of the component, $\mathbf{d}$ is the direction vector, $h$ is the height of the viewport, $w$ is the width of the viewport, and $\mathbf{p} _\mathrm{ref}$ is the reference camera position (see explanation above).
|
||||
|
||||
Applying the FOV-adjustment formulas from the previous section, we can easily calculate the real position that we have to set to the camera ($\mathbf{p} _\mathrm{adj}$):
|
||||
|
||||
For perspective projection: $\mathbf{p} _\mathrm{adj} = \mathbf{t} - \mathrm{normalize}(\mathbf{d}) \cdot \frac{r}{\sin(\alpha/2)} \cdot \max(1, \frac{h}{w})$
|
||||
|
||||
For orthographic projection: $\mathbf{p} _\mathrm{adj} = \mathbf{t} - \mathrm{normalize}(\mathbf{d}) \cdot \frac{r}{\tan(\alpha/2)} \cdot \max(1, \frac{h}{w})$
|
||||
|
||||
## `canvas` node
|
||||
|
||||
Attributes that apply to the MVS view as a whole, but are not related to camera positioning, can be set using a `canvas` node. This node is placed as a child of the `root` node (see [MVS tree schema](./mvs-tree-schema.md#canvas)).
|
||||
|
||||
Currently, this only includes one parameter: `background_color`. Its value can be set to either a [X11 color](http://www.w3.org/TR/css3-color/#svg-color) (e.g. `"red"`), or a hexadecimal color code (e.g. `"#FF0011"`). If there is no `canvas` node, the background will be white.
|
||||
565
docs/extensions/mvs/mvs-tree-schema.md
Normal file
565
docs/extensions/mvs/mvs-tree-schema.md
Normal file
@@ -0,0 +1,565 @@
|
||||
# MolViewSpec tree schema
|
||||
|
||||
(This documentation was auto-generated by `node lib/commonjs/cli/mvs/mvs-print-schema --markdown`)
|
||||
|
||||
## `root`
|
||||
|
||||
[Root of the tree must be of this kind]
|
||||
|
||||
Auxiliary node kind that only appears as the tree root.
|
||||
|
||||
Parent: none
|
||||
|
||||
Params: none
|
||||
|
||||
## `download`
|
||||
|
||||
This node instructs to retrieve a data resource.
|
||||
|
||||
Parent: `root`
|
||||
|
||||
Params:
|
||||
|
||||
- **`url: `**`string`
|
||||
|
||||
URL of the data resource.
|
||||
|
||||
## `parse`
|
||||
|
||||
This node instructs to parse a data resource.
|
||||
|
||||
Parent: `download`
|
||||
|
||||
Params:
|
||||
|
||||
- **`format: `**`"mmcif" | "bcif" | "pdb"`
|
||||
|
||||
Format of the input data resource.
|
||||
|
||||
## `structure`
|
||||
|
||||
This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.
|
||||
|
||||
Parent: `parse`
|
||||
|
||||
Params:
|
||||
|
||||
- **`type: `**`"model" | "assembly" | "symmetry" | "symmetry_mates"`
|
||||
|
||||
Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model).
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`model_index?: `**`Integer`
|
||||
|
||||
0-based index of model in case the input data contain multiple models.
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`assembly_id?: `**`string | null`
|
||||
|
||||
Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`radius?: `**`number`
|
||||
|
||||
Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).
|
||||
|
||||
Default: `5`
|
||||
|
||||
- **`ijk_min?: `**`[Integer, Integer, Integer]`
|
||||
|
||||
Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).
|
||||
|
||||
Default: `[-1, -1, -1]`
|
||||
|
||||
- **`ijk_max?: `**`[Integer, Integer, Integer]`
|
||||
|
||||
Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).
|
||||
|
||||
Default: `[1, 1, 1]`
|
||||
|
||||
## `transform`
|
||||
|
||||
This node instructs to rotate and/or translate structure coordinates.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`rotation?: `**`Array<number>`
|
||||
|
||||
Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).
|
||||
|
||||
Default: `[1, 0, 0, 0, 1, 0, 0, 0, 1]`
|
||||
|
||||
- **`translation?: `**`[number, number, number]`
|
||||
|
||||
Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).
|
||||
|
||||
Default: `[0, 0, 0]`
|
||||
|
||||
## `component`
|
||||
|
||||
This node instructs to create a component (i.e. a subset of the parent structure).
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`selector: `**`("all" | "polymer" | "protein" | "nucleic" | "branched" | "ligand" | "ion" | "water") | Partial<{ label_entity_id: string, label_asym_id: string, auth_asym_id: string, label_seq_id: Integer, auth_seq_id: Integer, pdbx_PDB_ins_code: string, beg_label_seq_id: Integer, end_label_seq_id: Integer, beg_auth_seq_id: Integer, end_auth_seq_id: Integer, label_atom_id: string, auth_atom_id: string, type_symbol: string, atom_id: Integer, atom_index: Integer }> | Array<Partial<{ label_entity_id: string, label_asym_id: string, auth_asym_id: string, label_seq_id: Integer, auth_seq_id: Integer, pdbx_PDB_ins_code: string, beg_label_seq_id: Integer, end_label_seq_id: Integer, beg_auth_seq_id: Integer, end_auth_seq_id: Integer, label_atom_id: string, auth_atom_id: string, type_symbol: string, atom_id: Integer, atom_index: Integer }>>`
|
||||
|
||||
Defines what part of the parent structure should be included in this component.
|
||||
|
||||
Default: `"all"`
|
||||
|
||||
## `component_from_uri`
|
||||
|
||||
This node instructs to create a component defined by an external annotation resource.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`uri: `**`string`
|
||||
|
||||
URL of the annotation resource.
|
||||
|
||||
- **`format: `**`"cif" | "bcif" | "json"`
|
||||
|
||||
Format of the annotation resource.
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"component"`
|
||||
|
||||
- **`field_values?: `**`Array<string> | null`
|
||||
|
||||
List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.
|
||||
|
||||
Default: `null`
|
||||
|
||||
## `component_from_source`
|
||||
|
||||
This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from. If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"component"`
|
||||
|
||||
- **`field_values?: `**`Array<string> | null`
|
||||
|
||||
List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.
|
||||
|
||||
Default: `null`
|
||||
|
||||
## `representation`
|
||||
|
||||
This node instructs to create a visual representation of a component.
|
||||
|
||||
Parent: `component` or `component_from_uri` or `component_from_source`
|
||||
|
||||
Params:
|
||||
|
||||
- **`type: `**`"ball_and_stick" | "cartoon" | "surface"`
|
||||
|
||||
Method of visual representation of the component.
|
||||
|
||||
## `color`
|
||||
|
||||
This node instructs to apply color to a visual representation.
|
||||
|
||||
Parent: `representation`
|
||||
|
||||
Params:
|
||||
|
||||
- **`color: `**`HexColor | ("aliceblue" | "antiquewhite" | "aqua" | "aquamarine" | "azure" | "beige" | "bisque" | "black" | "blanchedalmond" | "blue" | "blueviolet" | "brown" | "burlywood" | "cadetblue" | "chartreuse" | "chocolate" | "coral" | "cornflower" | "cornflowerblue" | "cornsilk" | "crimson" | "cyan" | "darkblue" | "darkcyan" | "darkgoldenrod" | "darkgray" | "darkgreen" | "darkgrey" | "darkkhaki" | "darkmagenta" | "darkolivegreen" | "darkorange" | "darkorchid" | "darkred" | "darksalmon" | "darkseagreen" | "darkslateblue" | "darkslategray" | "darkslategrey" | "darkturquoise" | "darkviolet" | "deeppink" | "deepskyblue" | "dimgray" | "dimgrey" | "dodgerblue" | "firebrick" | "floralwhite" | "forestgreen" | "fuchsia" | "gainsboro" | "ghostwhite" | "gold" | "goldenrod" | "gray" | "green" | "greenyellow" | "grey" | "honeydew" | "hotpink" | "indianred" | "indigo" | "ivory" | "khaki" | "laserlemon" | "lavender" | "lavenderblush" | "lawngreen" | "lemonchiffon" | "lightblue" | "lightcoral" | "lightcyan" | "lightgoldenrod" | "lightgoldenrodyellow" | "lightgray" | "lightgreen" | "lightgrey" | "lightpink" | "lightsalmon" | "lightseagreen" | "lightskyblue" | "lightslategray" | "lightslategrey" | "lightsteelblue" | "lightyellow" | "lime" | "limegreen" | "linen" | "magenta" | "maroon" | "maroon2" | "maroon3" | "mediumaquamarine" | "mediumblue" | "mediumorchid" | "mediumpurple" | "mediumseagreen" | "mediumslateblue" | "mediumspringgreen" | "mediumturquoise" | "mediumvioletred" | "midnightblue" | "mintcream" | "mistyrose" | "moccasin" | "navajowhite" | "navy" | "oldlace" | "olive" | "olivedrab" | "orange" | "orangered" | "orchid" | "palegoldenrod" | "palegreen" | "paleturquoise" | "palevioletred" | "papayawhip" | "peachpuff" | "peru" | "pink" | "plum" | "powderblue" | "purple" | "purple2" | "purple3" | "rebeccapurple" | "red" | "rosybrown" | "royalblue" | "saddlebrown" | "salmon" | "sandybrown" | "seagreen" | "seashell" | "sienna" | "silver" | "skyblue" | "slateblue" | "slategray" | "slategrey" | "snow" | "springgreen" | "steelblue" | "tan" | "teal" | "thistle" | "tomato" | "turquoise" | "violet" | "wheat" | "white" | "whitesmoke" | "yellow" | "yellowgreen")`
|
||||
|
||||
Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).
|
||||
|
||||
- **`selector?: `**`("all" | "polymer" | "protein" | "nucleic" | "branched" | "ligand" | "ion" | "water") | Partial<{ label_entity_id: string, label_asym_id: string, auth_asym_id: string, label_seq_id: Integer, auth_seq_id: Integer, pdbx_PDB_ins_code: string, beg_label_seq_id: Integer, end_label_seq_id: Integer, beg_auth_seq_id: Integer, end_auth_seq_id: Integer, label_atom_id: string, auth_atom_id: string, type_symbol: string, atom_id: Integer, atom_index: Integer }> | Array<Partial<{ label_entity_id: string, label_asym_id: string, auth_asym_id: string, label_seq_id: Integer, auth_seq_id: Integer, pdbx_PDB_ins_code: string, beg_label_seq_id: Integer, end_label_seq_id: Integer, beg_auth_seq_id: Integer, end_auth_seq_id: Integer, label_atom_id: string, auth_atom_id: string, type_symbol: string, atom_id: Integer, atom_index: Integer }>>`
|
||||
|
||||
Defines to what part of the representation this color should be applied.
|
||||
|
||||
Default: `"all"`
|
||||
|
||||
## `color_from_uri`
|
||||
|
||||
This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource.
|
||||
|
||||
Parent: `representation`
|
||||
|
||||
Params:
|
||||
|
||||
- **`uri: `**`string`
|
||||
|
||||
URL of the annotation resource.
|
||||
|
||||
- **`format: `**`"cif" | "bcif" | "json"`
|
||||
|
||||
Format of the annotation resource.
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"color"`
|
||||
|
||||
## `color_from_source`
|
||||
|
||||
This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.
|
||||
|
||||
Parent: `representation`
|
||||
|
||||
Params:
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from. If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"color"`
|
||||
|
||||
## `label`
|
||||
|
||||
This node instructs to add a label (textual visual representation) to a component.
|
||||
|
||||
Parent: `component` or `component_from_uri` or `component_from_source`
|
||||
|
||||
Params:
|
||||
|
||||
- **`text: `**`string`
|
||||
|
||||
Content of the shown label.
|
||||
|
||||
## `label_from_uri`
|
||||
|
||||
This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`uri: `**`string`
|
||||
|
||||
URL of the annotation resource.
|
||||
|
||||
- **`format: `**`"cif" | "bcif" | "json"`
|
||||
|
||||
Format of the annotation resource.
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"label"`
|
||||
|
||||
## `label_from_source`
|
||||
|
||||
This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from. If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"label"`
|
||||
|
||||
## `tooltip`
|
||||
|
||||
This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component).
|
||||
|
||||
Parent: `component` or `component_from_uri` or `component_from_source`
|
||||
|
||||
Params:
|
||||
|
||||
- **`text: `**`string`
|
||||
|
||||
Content of the shown tooltip.
|
||||
|
||||
## `tooltip_from_uri`
|
||||
|
||||
This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`uri: `**`string`
|
||||
|
||||
URL of the annotation resource.
|
||||
|
||||
- **`format: `**`"cif" | "bcif" | "json"`
|
||||
|
||||
Format of the annotation resource.
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"tooltip"`
|
||||
|
||||
## `tooltip_from_source`
|
||||
|
||||
This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.
|
||||
|
||||
Parent: `structure`
|
||||
|
||||
Params:
|
||||
|
||||
- **`schema: `**`"whole_structure" | "entity" | "chain" | "auth_chain" | "residue" | "auth_residue" | "residue_range" | "auth_residue_range" | "atom" | "auth_atom" | "all_atomic"`
|
||||
|
||||
Annotation schema defines what fields in the annotation will be taken into account.
|
||||
|
||||
- **`block_header?: `**`string | null`
|
||||
|
||||
Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`block_index?: `**`Integer`
|
||||
|
||||
0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).
|
||||
|
||||
Default: `0`
|
||||
|
||||
- **`category_name?: `**`string | null`
|
||||
|
||||
Name of the CIF category to read annotation from. If `null`, the first category in the block is used.
|
||||
|
||||
Default: `null`
|
||||
|
||||
- **`field_name?: `**`string`
|
||||
|
||||
Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).
|
||||
|
||||
Default: `"tooltip"`
|
||||
|
||||
## `focus`
|
||||
|
||||
This node instructs to set the camera focus to a component (zoom in).
|
||||
|
||||
Parent: `component` or `component_from_uri` or `component_from_source`
|
||||
|
||||
Params:
|
||||
|
||||
- **`direction?: `**`[number, number, number]`
|
||||
|
||||
Vector describing the direction of the view (camera position -> focused target).
|
||||
|
||||
Default: `[0, 0, -1]`
|
||||
|
||||
- **`up?: `**`[number, number, number]`
|
||||
|
||||
Vector which will be aligned with the screen Y axis.
|
||||
|
||||
Default: `[0, 1, 0]`
|
||||
|
||||
## `camera`
|
||||
|
||||
This node instructs to set the camera position and orientation.
|
||||
|
||||
Parent: `root`
|
||||
|
||||
Params:
|
||||
|
||||
- **`target: `**`[number, number, number]`
|
||||
|
||||
Coordinates of the point in space at which the camera is pointing.
|
||||
|
||||
- **`position: `**`[number, number, number]`
|
||||
|
||||
Coordinates of the camera.
|
||||
|
||||
- **`up?: `**`[number, number, number]`
|
||||
|
||||
Vector which will be aligned with the screen Y axis.
|
||||
|
||||
Default: `[0, 1, 0]`
|
||||
|
||||
## `canvas`
|
||||
|
||||
This node sets canvas properties.
|
||||
|
||||
Parent: `root`
|
||||
|
||||
Params:
|
||||
|
||||
- **`background_color: `**`HexColor | ("aliceblue" | "antiquewhite" | "aqua" | "aquamarine" | "azure" | "beige" | "bisque" | "black" | "blanchedalmond" | "blue" | "blueviolet" | "brown" | "burlywood" | "cadetblue" | "chartreuse" | "chocolate" | "coral" | "cornflower" | "cornflowerblue" | "cornsilk" | "crimson" | "cyan" | "darkblue" | "darkcyan" | "darkgoldenrod" | "darkgray" | "darkgreen" | "darkgrey" | "darkkhaki" | "darkmagenta" | "darkolivegreen" | "darkorange" | "darkorchid" | "darkred" | "darksalmon" | "darkseagreen" | "darkslateblue" | "darkslategray" | "darkslategrey" | "darkturquoise" | "darkviolet" | "deeppink" | "deepskyblue" | "dimgray" | "dimgrey" | "dodgerblue" | "firebrick" | "floralwhite" | "forestgreen" | "fuchsia" | "gainsboro" | "ghostwhite" | "gold" | "goldenrod" | "gray" | "green" | "greenyellow" | "grey" | "honeydew" | "hotpink" | "indianred" | "indigo" | "ivory" | "khaki" | "laserlemon" | "lavender" | "lavenderblush" | "lawngreen" | "lemonchiffon" | "lightblue" | "lightcoral" | "lightcyan" | "lightgoldenrod" | "lightgoldenrodyellow" | "lightgray" | "lightgreen" | "lightgrey" | "lightpink" | "lightsalmon" | "lightseagreen" | "lightskyblue" | "lightslategray" | "lightslategrey" | "lightsteelblue" | "lightyellow" | "lime" | "limegreen" | "linen" | "magenta" | "maroon" | "maroon2" | "maroon3" | "mediumaquamarine" | "mediumblue" | "mediumorchid" | "mediumpurple" | "mediumseagreen" | "mediumslateblue" | "mediumspringgreen" | "mediumturquoise" | "mediumvioletred" | "midnightblue" | "mintcream" | "mistyrose" | "moccasin" | "navajowhite" | "navy" | "oldlace" | "olive" | "olivedrab" | "orange" | "orangered" | "orchid" | "palegoldenrod" | "palegreen" | "paleturquoise" | "palevioletred" | "papayawhip" | "peachpuff" | "peru" | "pink" | "plum" | "powderblue" | "purple" | "purple2" | "purple3" | "rebeccapurple" | "red" | "rosybrown" | "royalblue" | "saddlebrown" | "salmon" | "sandybrown" | "seagreen" | "seashell" | "sienna" | "silver" | "skyblue" | "slateblue" | "slategray" | "slategrey" | "snow" | "springgreen" | "steelblue" | "tan" | "teal" | "thistle" | "tomato" | "turquoise" | "violet" | "wheat" | "white" | "whitesmoke" | "yellow" | "yellowgreen")`
|
||||
|
||||
Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).
|
||||
56
docs/extensions/mvs/selectors.md
Normal file
56
docs/extensions/mvs/selectors.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# MVS selectors
|
||||
|
||||
Selectors are used in MVS to define substructures (components) and apply colors, labels, or tooltips to them. MVS nodes that take a `selector` parameter are `component` (creates a component from the parent `structure` node) and `color` (applies coloring to a part of the parent `representation` node).
|
||||
|
||||
There are three kinds of selectors:
|
||||
|
||||
- **Static selector** is a string that selects a part of the structure based on entity type. The supported static selectors are these:
|
||||
|
||||
`"all", "polymer", "protein", "nucleic", "branched", "ligand", "ion", "water"`
|
||||
|
||||
- **Component expression** is an object that selects a set of atoms based on their properties like chain identifier, residue number, or type symbol. The type of a component expression object is:
|
||||
|
||||
```ts
|
||||
{
|
||||
label_entity_id?: str, // Entity identifier
|
||||
label_asym_id?: str, // Chain identifier in label_* numbering
|
||||
auth_asym_id?: str, // Chain identifier in auth_* numbering
|
||||
label_seq_id?: int, // Residue number in label_* numbering
|
||||
auth_seq_id?: int, // Residue number in auth_* numbering
|
||||
pdbx_PDB_ins_code?: str, // PDB insertion code
|
||||
beg_label_seq_id?: int, // Minimum label_seq_id (inclusive), leave blank to start from the beginning of the chain
|
||||
end_label_seq_id?: int, // Maximum label_seq_id (inclusive), leave blank to go to the end of the chain
|
||||
beg_auth_seq_id?: int, // Minimum auth_seq_id (inclusive), leave blank to start from the beginning of the chain
|
||||
end_auth_seq_id?: int, // Maximum auth_seq_id (inclusive), leave blank to go to the end of the chain
|
||||
label_atom_id?: str, // Atom name like 'CA', 'N', 'O', in label_* numbering
|
||||
auth_atom_id?: str, // Atom name like 'CA', 'N', 'O', in auth_* numbering
|
||||
type_symbol?: str, // Element symbol like 'H', 'HE', 'LI', 'BE'
|
||||
atom_id?: int, // Unique atom identifier (_atom_site.id)
|
||||
atom_index?: int, // 0-based index of the atom in the source data
|
||||
}
|
||||
```
|
||||
|
||||
A component expression can include any combination of the fields. An expression with multiple fields selects atoms that fulfill all fields at the same time. Examples:
|
||||
|
||||
```ts
|
||||
// Select whole chain A
|
||||
selector: { label_asym_id: 'A' }
|
||||
|
||||
// Select residues 100 to 200 (inclusive) in chain B
|
||||
selector: { label_asym_id: 'B', beg_label_seq_id: 100, end_label_seq_id: 200 }
|
||||
|
||||
// Select C-alpha atoms in residue 100 (using auth_* numbering) of any chain
|
||||
selector: { auth_seq_id: 100, type_symbol: 'C', auth_atom_id: 'CA' }
|
||||
```
|
||||
|
||||
- **Union component expression** is an array of simple component expressions. A union component expression is interpreted as set union, i.e. it selects all atoms that fulfill at least one of the expressions in the array. Example:
|
||||
|
||||
```ts
|
||||
// Select chains A, B, and C
|
||||
selector: [{ label_asym_id: 'A' }, { label_asym_id: 'B' }, { label_asym_id: 'C' }]
|
||||
|
||||
// Select residues up to 100 (inclusive) in chain A plus all magnesium atoms
|
||||
selector: [{ label_asym_id: 'A', end_label_seq_id: 100 }, { type_symbol: 'MG' }]
|
||||
```
|
||||
|
||||
An alternative to using selectors is using [MVS annotations](./annotations.md). This means defining the selections in a separate file and referencing them from the MVS file.
|
||||
118
docs/extensions/struct-conn.md
Normal file
118
docs/extensions/struct-conn.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# wwPDB StructConn extension
|
||||
|
||||
The STRUCT_CONN category in the mmCIF file format contains details about the connections between portions of the structure. These can be hydrogen bonds, salt bridges, disulfide bridges and so on (see more at <https://mmcif.wwpdb.org/dictionaries/mmcif_pdbx_v40.dic/Categories/struct_conn.html>).
|
||||
|
||||
**wwPDB StructConn extension** in Mol* provides functionality to retrieve and visualize these connections.
|
||||
|
||||
The extension exposes three functions, located in `src/extensions/wwpdb/struct-conn/index.ts`.
|
||||
|
||||
- `getStructConns` - to retrieve struct_conn records from a loaded structure
|
||||
- `inspectStructConn` - to visualize a struct_conn
|
||||
- `clearStructConnInspections` - to remove visulizations created by `inspectStructConn`
|
||||
|
||||
|
||||
## Example 1
|
||||
|
||||
The following example is a minimal HTML using this functionality:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer</title>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<body style="margin: 0px;">
|
||||
<div style="position: absolute; width: 100%; height: 10%; padding-block: 10px;">
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf1');">disulf1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'disulf2');">disulf2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale1');">covale1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale2');">covale2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale3');">covale3</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'covale4');">covale4</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc1');">metalc1</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc2');">metalc2</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc3');">metalc3</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, '5elb', 'metalc4');">metalc4</button>
|
||||
<button onclick="molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, '5elb');">CLEAR</button>
|
||||
</div>
|
||||
<div id="app" style="position: absolute; top: 10%; width: 100%; height: 90%;"></div>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var molstarViewer;
|
||||
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
|
||||
molstarViewer = viewer;
|
||||
viewer.loadPdb('5elb');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
The PDB ID (`'5elb'`) can be replaced be `undefined`, in which case the functions will apply to the first loaded structure.
|
||||
|
||||
|
||||
## Example 2
|
||||
|
||||
This is a more elaborated example, which automatically loads `5elb` (or any PDB entry given in the URL after `?pdb=`), retrieves the list of struct_conns, and creates a button for each struct_conn.
|
||||
|
||||
Be aware that some of the struct_conns may be present in the deposited model but not in the preferred assembly (default view). The presented example will raise a dialog window with error message in such cases, e.g. `disulf6` in entry `5elb`.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<title>Mol* Viewer - StructConn Extension Demo</title>
|
||||
<link rel="stylesheet" type="text/css" href="molstar.css" />
|
||||
</head>
|
||||
<style>
|
||||
body { margin: 0px; }
|
||||
#app { position: absolute; width: 85%; height: 100%; }
|
||||
#controls { position: absolute; right: 0; width: 15%; height: 100%; display: flex; flex-direction: column; overflow-y: scroll; }
|
||||
h1 { text-align: center; margin: 12px; font-weight: bold; font-size: 120%; }
|
||||
button { margin: 4px; margin-top: 0px; }
|
||||
</style>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<div id="controls">
|
||||
<h1 id="pdb-id">Loading...</h1>
|
||||
<button onclick="clearInspections();">CLEAR</button>
|
||||
</div>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
var pdbId = window.location.search.match(/[?&]pdb=(\w+)/i)?.[1]?.toLowerCase() ?? '5elb';
|
||||
var molstarViewer;
|
||||
function inspect(structConnId) {
|
||||
if (molstarViewer?.plugin) {
|
||||
molstar.PluginExtensions.wwPDBStructConn.inspectStructConn(molstarViewer.plugin, pdbId, structConnId).then(nSelectedAtoms => {
|
||||
if (nSelectedAtoms < 2) alert('Some of the interacting atoms were not found :(\n(maybe not present in the viewed assembly)');
|
||||
});
|
||||
}
|
||||
}
|
||||
function clearInspections() {
|
||||
if (molstarViewer?.plugin) {
|
||||
molstar.PluginExtensions.wwPDBStructConn.clearStructConnInspections(molstarViewer.plugin, pdbId);
|
||||
}
|
||||
}
|
||||
molstar.Viewer.create('app', { layoutIsExpanded: false }).then(viewer => {
|
||||
molstarViewer = viewer;
|
||||
return viewer.loadPdb(pdbId);
|
||||
}).then(() => {
|
||||
const structConns = molstar.PluginExtensions.wwPDBStructConn.getStructConns(molstarViewer.plugin, pdbId);
|
||||
const controls = document.getElementById('controls');
|
||||
for (const structConnId in structConns) {
|
||||
const button = document.createElement('button');
|
||||
button.innerText = structConnId;
|
||||
button.addEventListener('click', () => inspect(structConnId));
|
||||
controls.appendChild(button);
|
||||
};
|
||||
document.getElementById('pdb-id').innerHTML = pdbId;
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
@@ -26,6 +26,7 @@
|
||||
* Non-standard residues
|
||||
* Protein (1BRR, 5Z6Y)
|
||||
* DNA (5D3G)
|
||||
* Collagen (6JEC)
|
||||
* Multiple models with different sets of ligands or missing ligands (1J6T, 1VRC, 2ICY, 1O2F)
|
||||
* Long linear sugar chain (4HG6)
|
||||
* Anisotropic B-factors/Ellipsoids (1EJG)
|
||||
|
||||
75130
examples/7qpd.fw2.cif
Normal file
75130
examples/7qpd.fw2.cif
Normal file
File diff suppressed because it is too large
Load Diff
115
examples/mvs/1cbs-focus.mvsj
Normal file
115
examples/mvs/1cbs-focus.mvsj
Normal file
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Example MolViewSpec - 1cbs with labelled and zoomed ligand",
|
||||
"version": "1",
|
||||
"timestamp": "2023-11-24T10:45:49.873Z"
|
||||
},
|
||||
"root": {
|
||||
"kind": "root",
|
||||
"children": [
|
||||
{
|
||||
"kind": "download",
|
||||
"params": {
|
||||
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "parse",
|
||||
"params": {
|
||||
"format": "bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "structure",
|
||||
"params": {
|
||||
"type": "model"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "polymer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "cartoon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"color": "green"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"end_label_seq_id": 50
|
||||
},
|
||||
"color": "#6688ff"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Protein"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "ligand"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "focus",
|
||||
"params": {
|
||||
"direction": [0.5, 0, -1],
|
||||
"up": [0.365, 0.913, 0.183]
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "ball_and_stick"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"color": "#cc3399"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Retinoic Acid"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "canvas",
|
||||
"params": {
|
||||
"background_color": "#ffffee"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
117
examples/mvs/1cbs.mvsj
Normal file
117
examples/mvs/1cbs.mvsj
Normal file
@@ -0,0 +1,117 @@
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Example MolViewSpec - 1cbs with labelled protein and ligand",
|
||||
"version": "1",
|
||||
"timestamp": "2023-11-24T10:38:17.483Z"
|
||||
},
|
||||
"root": {
|
||||
"kind": "root",
|
||||
"children": [
|
||||
{
|
||||
"kind": "download",
|
||||
"params": {
|
||||
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1cbs.bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "parse",
|
||||
"params": {
|
||||
"format": "bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "structure",
|
||||
"params": {
|
||||
"type": "model"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "polymer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "cartoon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"color": "green"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 1,
|
||||
"end_label_seq_id": 50
|
||||
},
|
||||
"color": "#6688ff"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Protein"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "ligand"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "ball_and_stick"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"color": "#cc3399"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Retinoic Acid"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "canvas",
|
||||
"params": {
|
||||
"background_color": "#ffffee"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "camera",
|
||||
"params": {
|
||||
"target": [17, 21, 27],
|
||||
"position": [41, 34, 69],
|
||||
"up": [-0.129,0.966,-0.224]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
67
examples/mvs/1h9t_domain_colors.mvsj
Normal file
67
examples/mvs/1h9t_domain_colors.mvsj
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Example MolViewSpec - 1h9t colored by external annotation",
|
||||
"version": "1",
|
||||
"timestamp": "2023-11-24T10:47:33.182Z"
|
||||
},
|
||||
"root": {
|
||||
"kind": "root",
|
||||
"children": [
|
||||
{
|
||||
"kind": "download",
|
||||
"params": {
|
||||
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1h9t.bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "parse",
|
||||
"params": {
|
||||
"format": "bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "structure",
|
||||
"params": {
|
||||
"type": "model"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "polymer"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "cartoon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"selector": "all",
|
||||
"color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "color_from_uri",
|
||||
"params": {
|
||||
"uri": "./1h9t_domains.json",
|
||||
"format": "json",
|
||||
"schema": "all_atomic"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
583
examples/mvs/1h9t_domain_labels.mvsj
Normal file
583
examples/mvs/1h9t_domain_labels.mvsj
Normal file
@@ -0,0 +1,583 @@
|
||||
{
|
||||
"metadata": {
|
||||
"title": "Example MolViewSpec - 1h9t colored and labelled by external annotation",
|
||||
"version": "1",
|
||||
"timestamp": "2023-11-24T10:48:28.677Z"
|
||||
},
|
||||
"root": {
|
||||
"kind": "root",
|
||||
"children": [
|
||||
{
|
||||
"kind": "download",
|
||||
"params": {
|
||||
"url": "https://www.ebi.ac.uk/pdbe/entry-files/1h9t.bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "parse",
|
||||
"params": {
|
||||
"format": "bcif"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "structure",
|
||||
"params": {
|
||||
"type": "model"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "protein"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "cartoon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"selector": "all",
|
||||
"color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "color_from_uri",
|
||||
"params": {
|
||||
"uri": "./1h9t_domains.json",
|
||||
"format": "json",
|
||||
"schema": "all_atomic"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "nucleic"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "ball_and_stick"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color",
|
||||
"params": {
|
||||
"selector": "all",
|
||||
"color": "white"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "color_from_uri",
|
||||
"params": {
|
||||
"uri": "./1h9t_domains.json",
|
||||
"format": "json",
|
||||
"schema": "all_atomic"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "ion"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "representation",
|
||||
"params": {
|
||||
"type": "surface"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "color_from_uri",
|
||||
"params": {
|
||||
"uri": "./1h9t_domains.json",
|
||||
"format": "json",
|
||||
"schema": "all_atomic"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 9,
|
||||
"end_label_seq_id": 83
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA-binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 9,
|
||||
"end_label_seq_id": 83
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA-binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 84,
|
||||
"end_label_seq_id": 231
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Acyl-CoA\nbinding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 84,
|
||||
"end_label_seq_id": 231
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Acyl-CoA binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "C"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA X"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "D"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA Y"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "D",
|
||||
"atom_id": 4016
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA Y O5'"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "D",
|
||||
"atom_id": 4391
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "DNA Y O3'"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "E"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Gold"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "H"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Gold"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "F"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Chloride"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "G"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Chloride"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "I"
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Chloride"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 57
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 67
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 121
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 125
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 129
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 178
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 203,
|
||||
"end_label_seq_id": 205
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 67
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 121
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 125
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 129
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 178
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": {
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 203,
|
||||
"end_label_seq_id": 205
|
||||
}
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "label",
|
||||
"params": {
|
||||
"text": "Ligand binding"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "component",
|
||||
"params": {
|
||||
"selector": "all"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"kind": "focus",
|
||||
"params": {
|
||||
"direction": [-0.3, -0.1, -1]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "canvas",
|
||||
"params": {
|
||||
"background_color": "#eeffee"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
155
examples/mvs/1h9t_domains.json
Normal file
155
examples/mvs/1h9t_domains.json
Normal file
@@ -0,0 +1,155 @@
|
||||
[
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 9,
|
||||
"end_label_seq_id": 83,
|
||||
"color": "#dd6600",
|
||||
"tooltip": "DNA-binding"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 84,
|
||||
"end_label_seq_id": 231,
|
||||
"color": "#008800",
|
||||
"tooltip": "Acyl-CoA binding"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 9,
|
||||
"end_label_seq_id": 83,
|
||||
"color": "#cc8800",
|
||||
"tooltip": "DNA-binding"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 84,
|
||||
"end_label_seq_id": 231,
|
||||
"color": "#008888",
|
||||
"tooltip": "Acyl-CoA binding"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "C",
|
||||
"color": "#1100aa",
|
||||
"tooltip": "DNA X"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "D",
|
||||
"color": "#dddddd",
|
||||
"tooltip": "DNA Y"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "D",
|
||||
"atom_id": 4016,
|
||||
"color": "#ff0044",
|
||||
"tooltip": "DNA Y - O5'"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "D",
|
||||
"atom_id": 4391,
|
||||
"color": "#4400ff",
|
||||
"tooltip": "DNA Y - O3'"
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
"label_asym_id": "E",
|
||||
"color": "#ffff00",
|
||||
"tooltip": "Gold"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "H",
|
||||
"color": "#ffff00",
|
||||
"tooltip": "Gold"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "F",
|
||||
"color": "#00dd00",
|
||||
"tooltip": "Chloride"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "G",
|
||||
"color": "#00dd00",
|
||||
"tooltip": "Chloride"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "I",
|
||||
"color": "#00dd00",
|
||||
"tooltip": "Chloride"
|
||||
},
|
||||
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 57,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 67,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 121,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 125,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 129,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"label_seq_id": 178,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "A",
|
||||
"beg_label_seq_id": 203,
|
||||
"end_label_seq_id": 205,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 67,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 121,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 125,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"label_seq_id": 129,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
},
|
||||
{
|
||||
"label_asym_id": "B",
|
||||
"beg_label_seq_id": 203,
|
||||
"end_label_seq_id": 205,
|
||||
"color": "#ff0000",
|
||||
"tooltip": "Ligand binding site"
|
||||
}
|
||||
]
|
||||
16739
package-lock.json
generated
16739
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
108
package.json
108
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "3.31.2",
|
||||
"version": "3.44.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -48,6 +48,9 @@
|
||||
"bin": {
|
||||
"cif2bcif": "lib/commonjs/cli/cif2bcif/index.js",
|
||||
"cifschema": "lib/commonjs/cli/cifschema/index.js",
|
||||
"mvs-validate": "lib/commonjs/cli/mvs/mvs-validate.js",
|
||||
"mvs-render": "lib/commonjs/cli/mvs/mvs-render.js",
|
||||
"mvs-print-schema": "lib/commonjs/cli/mvs/mvs-print-schema.js",
|
||||
"model-server": "lib/commonjs/servers/model/server.js",
|
||||
"model-server-query": "lib/commonjs/servers/model/query.js",
|
||||
"model-server-preprocess": "lib/commonjs/servers/model/preprocess.js",
|
||||
@@ -95,74 +98,82 @@
|
||||
"Gianluca Tomasello <giagitom@gmail.com>",
|
||||
"Ke Ma <mark.ma@rcsb.org>",
|
||||
"Jason Pattle <jpattle@exscientia.co.uk>",
|
||||
"David Williams <dwilliams@nobiastx.com>"
|
||||
"David Williams <dwilliams@nobiastx.com>",
|
||||
"Zhenyu Zhang <jump2cn@gmail.com>",
|
||||
"Russell Parker <russell@benchling.com>",
|
||||
"Dominik Tichy <tichydominik451@gmail.com>",
|
||||
"Yana Rose <yana.v.rose@gmail.com>",
|
||||
"Yakov Pechersky <ffxen158@gmail.com>",
|
||||
"Christian Dominguez <christian.99dominguez@gmail.com>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@graphql-codegen/add": "^4.0.0",
|
||||
"@graphql-codegen/cli": "^3.0.0",
|
||||
"@graphql-codegen/time": "^4.0.0",
|
||||
"@graphql-codegen/typescript": "^3.0.0",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
|
||||
"@graphql-codegen/typescript-graphql-request": "^4.5.8",
|
||||
"@graphql-codegen/typescript-operations": "^3.0.0",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/gl": "^6.0.2",
|
||||
"@graphql-codegen/add": "^5.0.0",
|
||||
"@graphql-codegen/cli": "^5.0.0",
|
||||
"@graphql-codegen/time": "^5.0.0",
|
||||
"@graphql-codegen/typescript": "^4.0.1",
|
||||
"@graphql-codegen/typescript-graphql-files-modules": "^3.0.0",
|
||||
"@graphql-codegen/typescript-graphql-request": "^6.1.0",
|
||||
"@graphql-codegen/typescript-operations": "^4.0.1",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/jpeg-js": "^0.3.7",
|
||||
"@types/pngjs": "^6.0.1",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/react": "^18.0.27",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.50.0",
|
||||
"@typescript-eslint/parser": "^5.50.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/jest": "^29.5.11",
|
||||
"@types/react": "^18.2.47",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^6.18.0",
|
||||
"@typescript-eslint/parser": "^6.18.0",
|
||||
"benchmark": "^2.1.4",
|
||||
"concurrently": "^7.6.0",
|
||||
"cpx2": "^4.2.0",
|
||||
"concurrently": "^8.2.2",
|
||||
"cpx2": "^6.0.1",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"css-loader": "^6.7.3",
|
||||
"eslint": "^8.33.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"eslint": "^8.56.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.0",
|
||||
"graphql": "^16.6.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"graphql": "^16.8.1",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.4.1",
|
||||
"mini-css-extract-plugin": "^2.7.2",
|
||||
"jest": "^29.7.0",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"sass": "^1.58.0",
|
||||
"sass-loader": "^13.2.0",
|
||||
"simple-git": "^3.16.0",
|
||||
"sass": "^1.69.7",
|
||||
"sass-loader": "^13.3.3",
|
||||
"simple-git": "^3.22.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^29.0.5",
|
||||
"typescript": "^4.9.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1"
|
||||
"style-loader": "^3.3.3",
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.3.3",
|
||||
"webpack": "^5.89.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/argparse": "^2.0.10",
|
||||
"@types/benchmark": "^2.1.2",
|
||||
"@types/compression": "1.7.2",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/node": "^16.18.12",
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/swagger-ui-dist": "3.30.1",
|
||||
"@types/argparse": "^2.0.14",
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.7.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^16.18.69",
|
||||
"@types/node-fetch": "^2.6.10",
|
||||
"@types/swagger-ui-dist": "3.30.4",
|
||||
"argparse": "^2.0.1",
|
||||
"body-parser": "^1.20.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.18.2",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^9.0.19",
|
||||
"immutable": "^4.2.3",
|
||||
"node-fetch": "^2.6.9",
|
||||
"rxjs": "^7.8.0",
|
||||
"swagger-ui-dist": "^4.15.5",
|
||||
"tslib": "^2.5.0",
|
||||
"util.promisify": "^1.1.1",
|
||||
"immer": "^9.0.21",
|
||||
"immutable": "^4.3.4",
|
||||
"io-ts": "^2.2.21",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.10.5",
|
||||
"tslib": "^2.6.2",
|
||||
"util.promisify": "^1.1.2",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -170,6 +181,7 @@
|
||||
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"canvas": "^2.11.2",
|
||||
"gl": "^6.0.2",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"pngjs": "^6.0.0"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -15,7 +15,7 @@ const deployDir = path.resolve(buildDir, 'deploy/');
|
||||
const localPath = 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 -->`;
|
||||
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>`;
|
||||
|
||||
function log(command, stdout, stderr) {
|
||||
if (command) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -45,11 +45,13 @@ function occlusionStyle(plugin: PluginContext) {
|
||||
postprocessing: {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
samples: 32,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
} },
|
||||
outline: { name: 'on', params: {
|
||||
scale: 1.0,
|
||||
|
||||
@@ -6,35 +6,43 @@
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
import { CellPack } from '../../extensions/cellpack';
|
||||
import { DnatcoNtCs } from '../../extensions/dnatco';
|
||||
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { GeometryExport } from '../../extensions/geo-export';
|
||||
import { MAQualityAssessment } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { MAQualityAssessment, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
|
||||
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
|
||||
import { ModelExport } from '../../extensions/model-export';
|
||||
import { Mp4Export } from '../../extensions/mp4-export';
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVS } from '../../extensions/mvs/load';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBAssemblySymmetry, RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { RCSBAssemblySymmetryConfig } from '../../extensions/rcsb/assembly-symmetry/behavior';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } 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';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { Volume } from '../../mol-model/volume';
|
||||
import { DownloadStructure, PdbDownloadProvider } from '../../mol-plugin-state/actions/structure';
|
||||
import { DownloadDensity } from '../../mol-plugin-state/actions/volume';
|
||||
import { PresetTrajectoryHierarchy } from '../../mol-plugin-state/builder/structure/hierarchy-preset';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { BuiltInTopologyFormat } from '../../mol-plugin-state/formats/topology';
|
||||
import { BuiltInCoordinatesFormat } from '../../mol-plugin-state/formats/coordinates';
|
||||
import { BuiltInTrajectoryFormat } from '../../mol-plugin-state/formats/trajectory';
|
||||
import { BuildInVolumeFormat } from '../../mol-plugin-state/formats/volume';
|
||||
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
import { TrajectoryFromModelAndCoordinates } from '../../mol-plugin-state/transforms/model';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { PluginUIContext } from '../../mol-plugin-ui/context';
|
||||
import { createPluginUI } from '../../mol-plugin-ui/react18';
|
||||
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginConfig } from '../../mol-plugin/config';
|
||||
@@ -46,17 +54,15 @@ import { Asset } from '../../mol-util/assets';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import '../../mol-util/polyfill';
|
||||
import { ObjectKeys } from '../../mol-util/type-helpers';
|
||||
import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
|
||||
import { Backgrounds } from '../../extensions/backgrounds';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
|
||||
export { consoleStats, setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
|
||||
|
||||
const CustomFormats = [
|
||||
['g3d', G3dProvider] as const
|
||||
];
|
||||
|
||||
const Extensions = {
|
||||
export const ExtensionMap = {
|
||||
'volseg': PluginSpec.Behavior(Volseg),
|
||||
'backgrounds': PluginSpec.Behavior(Backgrounds),
|
||||
'cellpack': PluginSpec.Behavior(CellPack),
|
||||
@@ -71,11 +77,15 @@ const Extensions = {
|
||||
'geo-export': PluginSpec.Behavior(GeometryExport),
|
||||
'ma-quality-assessment': PluginSpec.Behavior(MAQualityAssessment),
|
||||
'zenodo-import': PluginSpec.Behavior(ZenodoImport),
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
customFormats: CustomFormats as [string, DataFormatProvider][],
|
||||
extensions: ObjectKeys(Extensions),
|
||||
extensions: ObjectKeys(ExtensionMap),
|
||||
disabledExtensions: [] as string[],
|
||||
layoutIsExpanded: true,
|
||||
layoutShowControls: true,
|
||||
layoutShowRemoteState: true,
|
||||
@@ -108,6 +118,9 @@ const DefaultViewerOptions = {
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerType: RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue,
|
||||
rcsbAssemblySymmetryDefaultServerUrl: RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue,
|
||||
rcsbAssemblySymmetryApplyColors: RCSBAssemblySymmetryConfig.ApplyColors.defaultValue,
|
||||
};
|
||||
type ViewerOptions = typeof DefaultViewerOptions;
|
||||
|
||||
@@ -126,11 +139,13 @@ export class Viewer {
|
||||
const o: ViewerOptions = { ...DefaultViewerOptions, ...definedOptions };
|
||||
const defaultSpec = DefaultPluginUISpec();
|
||||
|
||||
const disabledExtension = new Set(o.disabledExtensions ?? []);
|
||||
|
||||
const spec: PluginUISpec = {
|
||||
actions: defaultSpec.actions,
|
||||
behaviors: [
|
||||
...defaultSpec.behaviors,
|
||||
...o.extensions.map(e => Extensions[e]),
|
||||
...o.extensions.filter(e => !disabledExtension.has(e)).map(e => ExtensionMap[e]),
|
||||
],
|
||||
animations: [...defaultSpec.animations || []],
|
||||
customParamEditors: defaultSpec.customParamEditors,
|
||||
@@ -183,6 +198,9 @@ export class Viewer {
|
||||
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
|
||||
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
|
||||
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
|
||||
[RCSBAssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[RCSBAssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[RCSBAssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
]
|
||||
};
|
||||
|
||||
@@ -452,9 +470,34 @@ export class Viewer {
|
||||
return { model, coords, preset };
|
||||
}
|
||||
|
||||
async loadMvsFromUrl(url: string, format: 'mvsj') {
|
||||
if (format === 'mvsj') {
|
||||
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: url });
|
||||
} else {
|
||||
throw new Error(`Unknown MolViewSpec format: ${format}`);
|
||||
}
|
||||
// We might add more formats in the future
|
||||
}
|
||||
|
||||
async loadMvsData(data: string, format: 'mvsj') {
|
||||
if (format === 'mvsj') {
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
await loadMVS(this.plugin, mvsData, { sanityChecks: true, sourceUrl: undefined });
|
||||
} else {
|
||||
throw new Error(`Unknown MolViewSpec format: ${format}`);
|
||||
}
|
||||
// We might add more formats in the future
|
||||
}
|
||||
|
||||
handleResize() {
|
||||
this.plugin.layout.events.updated.next(void 0);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.plugin.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export interface LoadStructureOptions {
|
||||
@@ -503,8 +546,15 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
return await QualityAssessmentPLDDTPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => QualityAssessment.isApplicable(m, 'qmean'))) {
|
||||
return await QualityAssessmentQmeanPreset.apply(ref, params, plugin);
|
||||
} else if (!!structure.models.some(m => SbNcbrPartialChargesPropertyProvider.isApplicable(m))) {
|
||||
return await SbNcbrPartialChargesPreset.apply(ref, params, plugin);
|
||||
} else {
|
||||
return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS },
|
||||
};
|
||||
|
||||
@@ -65,7 +65,10 @@
|
||||
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
|
||||
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
|
||||
|
||||
molstar.Viewer.create('app', {
|
||||
disabledExtensions: [], // anything from Object.keys(molstar.ExtensionMap)
|
||||
layoutShowControls: !hideControls,
|
||||
viewportShowExpand: false,
|
||||
collapseLeftPanel: collapseLeftPanel,
|
||||
@@ -95,6 +98,14 @@
|
||||
var structureUrlIsBinary = getParam('structure-url-is-binary', '[^&]+').trim() === '1';
|
||||
if (structureUrl) viewer.loadStructureFromUrl(structureUrl, structureUrlFormat, structureUrlIsBinary);
|
||||
|
||||
var mvsUrl = getParam('mvs-url', '[^&]+').trim();
|
||||
var mvsData = getParam('mvs-data', '[^&]+').trim();
|
||||
var mvsFormat = getParam('mvs-format', '[^&]+').trim() || 'mvsj';
|
||||
if (mvsUrl && mvsData) console.error('Cannot specify mvs-url and mvs-data URL parameters at the same time. Ignoring both.');
|
||||
else if (mvsUrl) viewer.loadMvsFromUrl(mvsUrl, mvsFormat);
|
||||
else if (mvsData) viewer.loadMvsData(mvsData, mvsFormat);
|
||||
|
||||
|
||||
var pdb = getParam('pdb', '[^&]+').trim();
|
||||
if (pdb) viewer.loadPdb(pdb);
|
||||
|
||||
@@ -109,6 +120,11 @@
|
||||
|
||||
var modelArchive = getParam('model-archive', '[^&]+').trim();
|
||||
if (modelArchive) viewer.loadModelArchive(modelArchive);
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
// to aid GC
|
||||
viewer.dispose();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
|
||||
@@ -81,5 +81,5 @@ export const DefaultDataOptions: DataOptions = {
|
||||
const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
|
||||
const CCD_PATH = path.join(DATA_DIR, 'components.cif');
|
||||
const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
|
||||
const CCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'http://ftp.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
const CCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/components.cif';
|
||||
const PVCD_URL = 'https://files.wwpdb.org/pub/pdb/data/monomers/aa-variants-v1.cif';
|
||||
|
||||
@@ -22,6 +22,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'uline':
|
||||
case 'uchar3':
|
||||
case 'uchar1':
|
||||
case 'uchar5':
|
||||
// only force lower-case for enums
|
||||
return values && values.length ? EnumCol(values.map(x => x.toLowerCase()), 'lstr', description) : StrCol(description);
|
||||
case 'aliasname':
|
||||
@@ -61,6 +62,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'symop':
|
||||
case 'exp_data_doi':
|
||||
case 'asym_id':
|
||||
case 'uniprot_ptm_id':
|
||||
return StrCol(description);
|
||||
case 'int':
|
||||
case 'non_negative_int':
|
||||
@@ -89,6 +91,7 @@ export function getFieldType(type: string, description: string, values?: string[
|
||||
case 'Tag':
|
||||
case 'Implied':
|
||||
case 'Word':
|
||||
case 'Uri':
|
||||
return wrapContainer('str', ',', description, container);
|
||||
case 'Real':
|
||||
return wrapContainer('float', ',', description, container);
|
||||
|
||||
40
src/cli/mvs/mvs-print-schema.ts
Normal file
40
src/cli/mvs/mvs-print-schema.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for printing MolViewSpec tree schema
|
||||
* Build: npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-print-schema
|
||||
* node lib/commonjs/cli/mvs/mvs-print-schema --markdown
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
|
||||
import { MVSDefaults } from '../../extensions/mvs/tree/mvs/mvs-defaults';
|
||||
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
/** Command line argument values for `main` */
|
||||
interface Args {
|
||||
markdown: boolean,
|
||||
}
|
||||
|
||||
/** Return parsed command line arguments for `main` */
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Command-line application for printing MolViewSpec tree schema.' });
|
||||
parser.add_argument('-m', '--markdown', { action: 'store_true', help: 'Print the schema as markdown instead of plain text.' });
|
||||
const args = parser.parse_args();
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
/** Main workflow for printing MolViewSpec tree schema. */
|
||||
function main(args: Args) {
|
||||
if (args.markdown) {
|
||||
console.log(treeSchemaToMarkdown(MVSTreeSchema, MVSDefaults));
|
||||
} else {
|
||||
console.log(treeSchemaToString(MVSTreeSchema, MVSDefaults));
|
||||
}
|
||||
}
|
||||
|
||||
main(parseArguments());
|
||||
143
src/cli/mvs/mvs-render.ts
Normal file
143
src/cli/mvs/mvs-render.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for rendering images from MolViewSpec files
|
||||
* Build: npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
|
||||
* npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import gl from 'gl';
|
||||
import jpegjs from 'jpeg-js';
|
||||
import path from 'path';
|
||||
import pngjs from 'pngjs';
|
||||
|
||||
import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
|
||||
import { DefaultPluginSpec, PluginSpec } from '../../mol-plugin/spec';
|
||||
import { ExternalModules, defaultCanvas3DParams } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
import { onelinerJsonString } from '../../mol-util/json';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
// MolViewSpec must be imported after HeadlessPluginContext
|
||||
import { MolViewSpec } from '../../extensions/mvs/behavior';
|
||||
import { loadMVS } from '../../extensions/mvs/load';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { setCanvasModule } from '../../mol-geo/geometry/text/font-atlas';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
setCanvasModule(require('canvas'));
|
||||
|
||||
const DEFAULT_SIZE = '800x800';
|
||||
|
||||
/** Command line argument values for `main` */
|
||||
interface Args {
|
||||
input: string[],
|
||||
output: string[],
|
||||
size: { width: number, height: number },
|
||||
molj: boolean,
|
||||
}
|
||||
|
||||
/** Return parsed command line arguments for `main` */
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Command-line application for rendering images from MolViewSpec files' });
|
||||
parser.add_argument('-i', '--input', { required: true, nargs: '+', help: 'Input file(s) in .mvsj format' });
|
||||
parser.add_argument('-o', '--output', { required: true, nargs: '+', help: 'File path(s) for output files (one output path for each input file). Output format is inferred from the file extension (.png or .jpg)' });
|
||||
parser.add_argument('-s', '--size', { help: `Output image resolution, {width}x{height}. Default: ${DEFAULT_SIZE}.`, default: DEFAULT_SIZE });
|
||||
parser.add_argument('-m', '--molj', { action: 'store_true', help: `Save Mol* state (.molj) in addition to rendered images (use the same output file paths but with .molj extension)` });
|
||||
const args = parser.parse_args();
|
||||
try {
|
||||
const parts = args.size.split('x');
|
||||
if (parts.length !== 2) throw new Error('Must contain two x-separated parts');
|
||||
args.size = { width: parseIntStrict(parts[0]), height: parseIntStrict(parts[1]) };
|
||||
} catch {
|
||||
parser.error(`argument: --size: invalid image size string: '${args.size}' (must be two x-separated integers (width and height), e.g. '400x300')`);
|
||||
}
|
||||
if (args.input.length !== args.output.length) {
|
||||
parser.error(`argument: --output: must specify the same number of input and output file paths (specified ${args.input.length} input path${args.input.length !== 1 ? 's' : ''} but ${args.output.length} output path${args.output.length !== 1 ? 's' : ''})`);
|
||||
}
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
/** Main workflow for rendering images from MolViewSpec files */
|
||||
async function main(args: Args): Promise<void> {
|
||||
const plugin = await createHeadlessPlugin(args);
|
||||
|
||||
for (let i = 0; i < args.input.length; i++) {
|
||||
const input = args.input[i];
|
||||
const output = args.output[i];
|
||||
console.log(`Processing ${input} -> ${output}`);
|
||||
|
||||
const data = fs.readFileSync(input, { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
|
||||
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: `file://${path.resolve(input)}` });
|
||||
fs.mkdirSync(path.dirname(output), { recursive: true });
|
||||
if (args.molj) {
|
||||
await plugin.saveStateSnapshot(withExtension(output, '.molj'));
|
||||
}
|
||||
await plugin.saveImage(output);
|
||||
checkState(plugin);
|
||||
}
|
||||
await plugin.clear();
|
||||
plugin.dispose();
|
||||
}
|
||||
|
||||
/** Return a new and initiatized HeadlessPlugin */
|
||||
async function createHeadlessPlugin(args: Pick<Args, 'size'>): Promise<HeadlessPluginContext> {
|
||||
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
|
||||
const spec = DefaultPluginSpec();
|
||||
spec.behaviors.push(PluginSpec.Behavior(MolViewSpec));
|
||||
const headlessCanvasOptions = defaultCanvas3DParams();
|
||||
const canvasOptions = {
|
||||
...PD.getDefaultValues(Canvas3DParams),
|
||||
cameraResetDurationMs: headlessCanvasOptions.cameraResetDurationMs,
|
||||
postprocessing: headlessCanvasOptions.postprocessing,
|
||||
};
|
||||
const plugin = new HeadlessPluginContext(externalModules, spec, args.size, { canvas: canvasOptions });
|
||||
try {
|
||||
await plugin.init();
|
||||
} catch (error) {
|
||||
plugin.dispose();
|
||||
throw error;
|
||||
}
|
||||
return plugin;
|
||||
}
|
||||
|
||||
/** Parse integer, fail early. */
|
||||
function parseIntStrict(str: string): number {
|
||||
if (str === '') throw new Error('Is empty string');
|
||||
const result = Number(str);
|
||||
if (isNaN(result)) throw new Error('Is NaN');
|
||||
if (Math.floor(result) !== result) throw new Error('Is not integer');
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Replace the file extension in `filename` by `extension`. If `filename` has no extension, add it. */
|
||||
function withExtension(filename: string, extension: string): string {
|
||||
const oldExtension = path.extname(filename);
|
||||
return filename.slice(0, -oldExtension.length) + extension;
|
||||
}
|
||||
|
||||
/** Check Mol* state, print and throw error if any cell is not OK. */
|
||||
function checkState(plugin: PluginContext): void {
|
||||
const cells = Array.from(plugin.state.data.cells.values());
|
||||
const badCell = cells.find(cell => cell.status !== 'ok');
|
||||
if (badCell) {
|
||||
console.error(`Building Mol* state failed`);
|
||||
console.error(` Transformer: ${badCell.transform.transformer.id}`);
|
||||
console.error(` Params: ${onelinerJsonString(badCell.transform.params)}`);
|
||||
console.error(` Error: ${badCell.errorText}`);
|
||||
console.error(``);
|
||||
throw new Error(`Building Mol* state failed: ${badCell.errorText}`);
|
||||
}
|
||||
}
|
||||
|
||||
main(parseArguments());
|
||||
57
src/cli/mvs/mvs-validate.ts
Normal file
57
src/cli/mvs/mvs-validate.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*
|
||||
* Command-line application for validating MolViewSpec files
|
||||
* Build: npm run build
|
||||
* Run: node lib/commonjs/cli/mvs/mvs-validate examples/mvs/1cbs.mvsj
|
||||
*/
|
||||
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
/** Command line argument values for `main` */
|
||||
interface Args {
|
||||
input: string[],
|
||||
no_extra: boolean,
|
||||
}
|
||||
|
||||
/** Return parsed command line arguments for `main` */
|
||||
function parseArguments(): Args {
|
||||
const parser = new ArgumentParser({ description: 'Command-line application for validating MolViewSpec files. Prints validation status (OK/FAILED) to stdout, detailed validation issues to stderr. Exits with a zero exit code if all input files are OK.' });
|
||||
parser.add_argument('input', { nargs: '+', help: 'Input file(s) in .mvsj format' });
|
||||
parser.add_argument('--no-extra', { action: 'store_true', help: 'Treat presence of extra node params as an issue.' });
|
||||
const args = parser.parse_args();
|
||||
return { ...args };
|
||||
}
|
||||
|
||||
/** Main workflow for validating MolViewSpec files. Returns the number of failed input files. */
|
||||
function main(args: Args): number {
|
||||
let nFailed = 0;
|
||||
for (const input of args.input) {
|
||||
const data = fs.readFileSync(input, { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
const issues = MVSData.validationIssues(mvsData, { noExtra: args.no_extra });
|
||||
const status = issues ? 'FAILED' : 'OK';
|
||||
console.log(`${status.padEnd(6)} ${input}`);
|
||||
if (issues) {
|
||||
nFailed++;
|
||||
for (const issue of issues) {
|
||||
console.error(issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nFailed;
|
||||
}
|
||||
|
||||
const nFailed = main(parseArguments());
|
||||
if (nFailed > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
@@ -46,6 +46,14 @@ class BasicWrapper {
|
||||
this.plugin.representation.structure.themes.colorThemeRegistry.add(CustomColorThemeProvider);
|
||||
this.plugin.managers.lociLabels.addProvider(StripedResidues.labelProvider!);
|
||||
this.plugin.customModelProperties.register(StripedResidues.propertyProvider, true);
|
||||
|
||||
this.plugin.managers.dragAndDrop.addHandler('custom-wrapper', (files) => {
|
||||
if (files.some(f => f.name.toLowerCase().endsWith('.testext'))) {
|
||||
console.log('.testext File dropped');
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
async load({ url, format = 'mmcif', isBinary = false, assemblyId = '' }: LoadParams) {
|
||||
|
||||
@@ -12,15 +12,21 @@
|
||||
import { ArgumentParser } from 'argparse';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import gl from 'gl';
|
||||
import pngjs from 'pngjs';
|
||||
import jpegjs from 'jpeg-js';
|
||||
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
|
||||
import { DefaultPluginSpec } from '../../mol-plugin/spec';
|
||||
import { STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
|
||||
import { setFSModule } from '../../mol-util/data-source';
|
||||
|
||||
|
||||
setFSModule(fs);
|
||||
|
||||
interface Args {
|
||||
pdbId: string,
|
||||
outDirectory: string
|
||||
@@ -42,7 +48,8 @@ async function main() {
|
||||
console.log('Outputs:', args.outDirectory);
|
||||
|
||||
// Create a headless plugin
|
||||
const plugin = new HeadlessPluginContext(DefaultPluginSpec(), { width: 800, height: 800 });
|
||||
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
|
||||
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec(), { width: 800, height: 800 });
|
||||
await plugin.init();
|
||||
|
||||
// Download and visualize data in the plugin
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -24,9 +24,31 @@ const Canvas3DPresets = {
|
||||
illustrative: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'on', params: { scale: 1, threshold: 0.33, color: Color(0x000000), includeTransparent: true, } },
|
||||
shadow: { name: 'off', params: {} },
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.33,
|
||||
color: Color(0x000000),
|
||||
includeTransparent: true,
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 1.0,
|
||||
@@ -37,9 +59,25 @@ const Canvas3DPresets = {
|
||||
occlusion: {
|
||||
canvas3d: <Preset>{
|
||||
postprocessing: {
|
||||
occlusion: { name: 'on', params: { samples: 32, radius: 6, bias: 1.4, blurKernelSize: 15, resolutionScale: 1 } },
|
||||
outline: { name: 'off', params: {} },
|
||||
shadow: { name: 'off', params: {} },
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
shadow: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
},
|
||||
},
|
||||
renderer: {
|
||||
ambientIntensity: 0.4,
|
||||
|
||||
@@ -50,7 +50,7 @@ const _testBasis: Basis = {
|
||||
0.025886090588624934,
|
||||
0.019164790004065606,
|
||||
-0.013539970104105408
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
@@ -101,7 +101,7 @@ const _testBasis: Basis = {
|
||||
0.5082729578468134,
|
||||
1.6880351220025265,
|
||||
0.4963443067810461
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
@@ -158,7 +158,7 @@ const _testBasis: Basis = {
|
||||
1.1367367844436005,
|
||||
-0.47018519422670163,
|
||||
-1.356802622574504
|
||||
] as Vec3,
|
||||
],
|
||||
'shells': [
|
||||
{
|
||||
'angularMomentum': [0],
|
||||
|
||||
@@ -53,7 +53,7 @@ export async function sphericalCollocation(
|
||||
L,
|
||||
shell.coefficients[amIndex++],
|
||||
shell.exponents,
|
||||
atom.center,
|
||||
atom.center as unknown as Vec3,
|
||||
cutoffThreshold,
|
||||
alpha
|
||||
);
|
||||
|
||||
@@ -22,7 +22,7 @@ export interface SphericalElectronShell {
|
||||
export interface Basis {
|
||||
atoms: {
|
||||
// in Bohr units!
|
||||
center: Vec3;
|
||||
center: [number, number, number];
|
||||
shells: SphericalElectronShell[];
|
||||
}[];
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export function initCubeGrid(params: CubeGridComputationParams): CubeGridInfo {
|
||||
const count = geometry.length;
|
||||
const box = Box3D.expand(
|
||||
Box3D(),
|
||||
Box3D.fromVec3Array(Box3D(), geometry),
|
||||
Box3D.fromVec3Array(Box3D(), geometry as unknown as Vec3[]),
|
||||
Vec3.create(expand, expand, expand)
|
||||
);
|
||||
const size = Box3D.size(Vec3(), box);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -28,6 +28,12 @@ export const Backgrounds = PluginBehavior.create<{ }>({
|
||||
ctor: class extends PluginBehavior.Handler<{ }> {
|
||||
register(): void {
|
||||
this.ctx.config.set(PluginConfig.Background.Styles, [
|
||||
[{
|
||||
variant: {
|
||||
name: 'off',
|
||||
params: {}
|
||||
}
|
||||
}, 'Off'],
|
||||
[{
|
||||
variant: {
|
||||
name: 'radialGradient',
|
||||
@@ -50,6 +56,7 @@ export const Backgrounds = PluginBehavior.create<{ }>({
|
||||
lightness: 0,
|
||||
saturation: 0,
|
||||
opacity: 1,
|
||||
blur: 0,
|
||||
coverage: 'viewport',
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,14 @@ export function CellPackGenerateColorTheme(ctx: ThemeDataContext, props: PD.Valu
|
||||
const palette = getPalette(size, { palette: {
|
||||
name: 'generate',
|
||||
params: {
|
||||
hue, chroma: [30, 80], luminance: [15, 85],
|
||||
clusteringStepCount: 50, minSampleCount: 800, maxCount: 75
|
||||
hue,
|
||||
chroma: [30, 80],
|
||||
luminance: [15, 85],
|
||||
clusteringStepCount: 50,
|
||||
minSampleCount: 800,
|
||||
maxCount: 75,
|
||||
sampleCountFactor: 5,
|
||||
sort: 'contrast'
|
||||
}
|
||||
} }, { minLabel: 'Min', maxLabel: 'Max' });
|
||||
legend = palette.legend;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
@@ -600,10 +600,12 @@ export const LoadCellPackModel = StateAction.build({
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 8,
|
||||
bias: 1,
|
||||
blurKernelSize: 15,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
|
||||
@@ -31,7 +31,8 @@ type NtCTubeColors = typeof NtCTubeColors;
|
||||
export const NtCTubeColorThemeParams = {
|
||||
colors: PD.MappedStatic('default', {
|
||||
'default': PD.EmptyGroup(),
|
||||
'custom': PD.Group(getColorMapParams(NtCTubeColors))
|
||||
'custom': PD.Group(getColorMapParams(NtCTubeColors)),
|
||||
'uniform': PD.Color(Color(0xEEEEEE)),
|
||||
}),
|
||||
markResidueBoundaries: PD.Boolean(true),
|
||||
markSegmentBoundaries: PD.Boolean(true),
|
||||
@@ -43,7 +44,15 @@ export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
|
||||
}
|
||||
|
||||
export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
|
||||
const colorMap = props.colors.name === 'default' ? NtCTubeColors : props.colors.params;
|
||||
const colorMap = props.colors.name === 'default'
|
||||
? NtCTubeColors
|
||||
: props.colors.name === 'custom'
|
||||
? props.colors.params
|
||||
: ColorMap({
|
||||
...Object.fromEntries(ObjectKeys(NtCTubeColors).map(item => [item, props.colors.params])),
|
||||
residueMarker: NtCTubeColors.residueMarker,
|
||||
stepBoundaryMarker: NtCTubeColors.stepBoundaryMarker
|
||||
}) as NtCTubeColors;
|
||||
|
||||
function color(location: Location, isSecondary: boolean): Color {
|
||||
if (NTT.isLocation(location)) {
|
||||
|
||||
@@ -316,7 +316,7 @@ function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure,
|
||||
radiusTop: diameter / 2, radiusBottom: diameter / 2, topCap: true, bottomCap: true, radialSegments: segCount.radial,
|
||||
};
|
||||
mb.currentGroup = FirstBlockId;
|
||||
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, cylinderProps);
|
||||
addFixedCountDashedCylinder(mb, p_1, p1, 1, 2 * segCount.linear, false, cylinderProps);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,11 +14,12 @@ import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Un
|
||||
|
||||
function getAtomPosition(vec: Vec3, loc: StructureElement.Location, residue: DnatcoUtil.Residue, names: string[], altId: string, insCode: string) {
|
||||
const eI = DnatcoUtil.getAtomIndex(loc, residue, names, altId, insCode);
|
||||
if (eI !== -1)
|
||||
if (eI !== -1) {
|
||||
loc.unit.conformation.invariantPosition(eI, vec);
|
||||
else {
|
||||
vec[0] = 0; vec[1] = 0; vec[2] = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false; // Atom not found
|
||||
}
|
||||
|
||||
const p_1 = Vec3();
|
||||
@@ -29,19 +30,38 @@ const p3 = Vec3();
|
||||
const p4 = Vec3();
|
||||
const pP = Vec3();
|
||||
|
||||
const C5PrimeNames = ['C5\'', 'C5*'];
|
||||
const O3PrimeNames = ['O3\'', 'O3*'];
|
||||
const O5PrimeNames = ['O5\'', 'O5*'];
|
||||
const PNames = ['P'];
|
||||
|
||||
function getPoints(
|
||||
loc: StructureElement.Location,
|
||||
r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue,
|
||||
altId0: string, altId1: string, altId2: string,
|
||||
insCode0: string, insCode1: string, insCode2: string,
|
||||
) {
|
||||
if (r0) getAtomPosition(p_1, loc, r0, ['C5\'', 'C5*'], altId0, insCode0);
|
||||
r0 ? getAtomPosition(p0, loc, r0, ['O3\'', 'O3*'], altId0, insCode0) : getAtomPosition(p0, loc, r1, ['O5\'', 'O5*'], altId1, insCode1);
|
||||
getAtomPosition(p1, loc, r1, ['C5\'', 'C5*'], altId1, insCode1);
|
||||
getAtomPosition(p2, loc, r1, ['O3\'', 'O3*'], altId1, insCode1);
|
||||
getAtomPosition(p3, loc, r2, ['C5\'', 'C5*'], altId2, insCode2);
|
||||
getAtomPosition(p4, loc, r2, ['O3\'', 'O3*'], altId2, insCode2);
|
||||
getAtomPosition(pP, loc, r2, ['P'], altId2, insCode2);
|
||||
if (r0) {
|
||||
if (!getAtomPosition(p_1, loc, r0, C5PrimeNames, altId0, insCode0))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p0, loc, r0, O3PrimeNames, altId0, insCode0))
|
||||
return void 0;
|
||||
} else {
|
||||
if (!getAtomPosition(p0, loc, r1, O5PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
}
|
||||
|
||||
if (!getAtomPosition(p1, loc, r1, C5PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p2, loc, r1, O3PrimeNames, altId1, insCode1))
|
||||
return void 0;
|
||||
|
||||
if (!getAtomPosition(p3, loc, r2, C5PrimeNames, altId2, insCode2))
|
||||
return void 0;
|
||||
if (!getAtomPosition(p4, loc, r2, O3PrimeNames, altId2, insCode2))
|
||||
return void 0;
|
||||
if (!getAtomPosition(pP, loc, r2, PNames, altId2, insCode2))
|
||||
return void 0;
|
||||
|
||||
return { p_1, p0, p1, p2, p3, p4, pP };
|
||||
}
|
||||
@@ -142,9 +162,12 @@ export class NtCTubeSegmentsIterator {
|
||||
const insCodeTwo = step.PDB_ins_code_2;
|
||||
const followsGap = !!r0 && hasGapElements(r0, this.loc.unit) && hasGapElements(r1, this.loc.unit);
|
||||
const precedesDiscontinuity = r3 ? r3.index !== r2.index + 1 : false;
|
||||
const points = getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo);
|
||||
if (!points)
|
||||
return void 0;
|
||||
|
||||
return {
|
||||
...getPoints(this.loc, r0, r1, r2, altIdPrev, this.altIdOne, altIdTwo, insCodePrev, this.insCodeOne, insCodeTwo),
|
||||
...points,
|
||||
stepIdx,
|
||||
followsGap,
|
||||
firstInChain: !r0,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -13,7 +13,7 @@ import { PLUGIN_VERSION } from '../../mol-plugin/version';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { fillSerial } from '../../mol-util/array';
|
||||
import { NumberArray } from '../../mol-util/type-helpers';
|
||||
import { NumberArray, assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { MeshExporter, AddMeshInput, MeshGeoData } from './mesh-exporter';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
@@ -35,6 +35,15 @@ const BIN_CHUNK_TYPE = 0x004E4942;
|
||||
const JSON_PAD_CHAR = 0x20;
|
||||
const BIN_PAD_CHAR = 0x00;
|
||||
|
||||
function getPrimitiveMode(mode: 'points' | 'lines' | 'triangles'): number {
|
||||
switch (mode) {
|
||||
case 'points': return 0;
|
||||
case 'lines': return 1;
|
||||
case 'triangles': return 4;
|
||||
default: assertUnreachable(mode);
|
||||
}
|
||||
}
|
||||
|
||||
export type GlbData = {
|
||||
glb: Uint8Array
|
||||
}
|
||||
@@ -89,12 +98,12 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return accessorOffset;
|
||||
}
|
||||
|
||||
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
|
||||
private addGeometryBuffers(vertices: Float32Array, normals: Float32Array | undefined, indices: Uint32Array | undefined, vertexCount: number, drawCount: number, isGeoTexture: boolean) {
|
||||
const tmpV = Vec3();
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
|
||||
const vertexArray = new Float32Array(vertexCount * 3);
|
||||
const normalArray = new Float32Array(vertexCount * 3);
|
||||
let normalArray: Float32Array | undefined;
|
||||
let indexArray: Uint32Array | undefined;
|
||||
|
||||
// position
|
||||
@@ -104,32 +113,35 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
}
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, normals, i * stride);
|
||||
v3normalize(tmpV, tmpV);
|
||||
v3toArray(tmpV, normalArray, i * 3);
|
||||
if (normals) {
|
||||
normalArray = new Float32Array(vertexCount * 3);
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(tmpV, normals, i * stride);
|
||||
v3normalize(tmpV, tmpV);
|
||||
v3toArray(tmpV, normalArray, i * 3);
|
||||
}
|
||||
}
|
||||
|
||||
// face
|
||||
if (!isGeoTexture) {
|
||||
indexArray = indices!.slice(0, drawCount);
|
||||
if (!isGeoTexture && indices) {
|
||||
indexArray = indices.slice(0, drawCount);
|
||||
}
|
||||
|
||||
const [vertexMin, vertexMax] = GlbExporter.vec3MinMax(vertexArray);
|
||||
|
||||
let vertexBuffer = vertexArray.buffer;
|
||||
let normalBuffer = normalArray.buffer;
|
||||
let indexBuffer = isGeoTexture ? undefined : indexArray!.buffer;
|
||||
let normalBuffer = normalArray?.buffer;
|
||||
let indexBuffer = (isGeoTexture || !indexArray) ? undefined : indexArray.buffer;
|
||||
if (!IsNativeEndianLittle) {
|
||||
vertexBuffer = flipByteOrder(new Uint8Array(vertexBuffer), 4);
|
||||
normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
|
||||
if (normalBuffer) normalBuffer = flipByteOrder(new Uint8Array(normalBuffer), 4);
|
||||
if (!isGeoTexture) indexBuffer = flipByteOrder(new Uint8Array(indexBuffer!), 4);
|
||||
}
|
||||
|
||||
return {
|
||||
vertexAccessorIndex: this.addBuffer(vertexBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER, vertexMin, vertexMax),
|
||||
normalAccessorIndex: this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER),
|
||||
indexAccessorIndex: isGeoTexture ? undefined : this.addBuffer(indexBuffer!, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
|
||||
normalAccessorIndex: normalBuffer ? this.addBuffer(normalBuffer, FLOAT, 'VEC3', vertexCount, ARRAY_BUFFER) : undefined,
|
||||
indexAccessorIndex: (isGeoTexture || !indexBuffer) ? undefined : this.addBuffer(indexBuffer, UNSIGNED_INT, 'SCALAR', drawCount, ELEMENT_ARRAY_BUFFER)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,8 +170,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
return this.addBuffer(colorBuffer, UNSIGNED_BYTE, 'VEC4', vertexCount, ARRAY_BUFFER, undefined, undefined, true);
|
||||
}
|
||||
|
||||
private addMaterial(metalness: number, roughness: number) {
|
||||
const hash = `${metalness}|${roughness}`;
|
||||
private addMaterial(metalness: number, roughness: number, doubleSided: boolean, alpha: boolean) {
|
||||
const hash = `${metalness}|${roughness}|${doubleSided}`;
|
||||
if (!this.materialMap.has(hash)) {
|
||||
this.materialMap.set(hash, this.materials.length);
|
||||
this.materials.push({
|
||||
@@ -167,14 +179,16 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
baseColorFactor: [1, 1, 1, 1],
|
||||
metallicFactor: metalness,
|
||||
roughnessFactor: roughness
|
||||
}
|
||||
},
|
||||
doubleSided,
|
||||
alphaMode: alpha ? 'BLEND' : 'OPAQUE',
|
||||
});
|
||||
}
|
||||
return this.materialMap.get(hash)!;
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
|
||||
const t = Mat4();
|
||||
|
||||
@@ -186,32 +200,34 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
const metalness = values.uMetalness.ref.value;
|
||||
const roughness = values.uRoughness.ref.value;
|
||||
const doubleSided = values.uDoubleSided?.ref.value || values.hasReflection.ref.value;
|
||||
const alpha = values.uAlpha.ref.value < 1;
|
||||
|
||||
const material = this.addMaterial(metalness, roughness);
|
||||
const material = this.addMaterial(metalness, roughness, doubleSided, alpha);
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedColors = GlbExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
interpolatedColors = GlbExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
interpolatedOverpaint = GlbExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = GlbExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
// instancing
|
||||
const sameGeometryBuffers = mesh !== undefined;
|
||||
const sameColorBuffer = sameGeometryBuffers && colorType !== 'instance' && !colorType.endsWith('Instance') && !dTransparency;
|
||||
let vertexAccessorIndex: number;
|
||||
let normalAccessorIndex: number;
|
||||
let normalAccessorIndex: number | undefined;
|
||||
let indexAccessorIndex: number | undefined;
|
||||
let colorAccessorIndex: number;
|
||||
let meshIndex: number;
|
||||
@@ -235,7 +251,7 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
|
||||
// create a color buffer if needed
|
||||
if (instanceIndex === 0 || !sameColorBuffer) {
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
colorAccessorIndex = this.addColorBuffer({ values, groups, vertexCount, instanceIndex, isGeoTexture, mode }, interpolatedColors, interpolatedOverpaint, interpolatedTransparency);
|
||||
}
|
||||
|
||||
// glTF mesh
|
||||
@@ -248,7 +264,8 @@ export class GlbExporter extends MeshExporter<GlbData> {
|
||||
COLOR_0: colorAccessorIndex!
|
||||
},
|
||||
indices: indexAccessorIndex,
|
||||
material
|
||||
material,
|
||||
mode: getPrimitiveMode(mode),
|
||||
}]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -28,24 +28,32 @@ import { Color } from '../../mol-util/color/color';
|
||||
import { unpackRGBToInt } from '../../mol-util/number-packing';
|
||||
import { RenderObjectExporter, RenderObjectExportData } from './render-object-exporter';
|
||||
import { readAlphaTexture, readTexture } from '../../mol-gl/compute/util';
|
||||
import { assertUnreachable } from '../../mol-util/type-helpers';
|
||||
import { ValueCell } from '../../mol-util/value-cell';
|
||||
|
||||
const GeoExportName = 'geo-export';
|
||||
|
||||
// avoiding namespace lookup improved performance in Chrome (Aug 2020)
|
||||
const v3fromArray = Vec3.fromArray;
|
||||
const v3sub = Vec3.sub;
|
||||
const v3dot = Vec3.dot;
|
||||
const v3unitY = Vec3.unitY;
|
||||
|
||||
type MeshMode = 'points' | 'lines' | 'triangles'
|
||||
|
||||
export interface AddMeshInput {
|
||||
mesh: {
|
||||
vertices: Float32Array
|
||||
normals: Float32Array
|
||||
normals: Float32Array | undefined
|
||||
indices: Uint32Array | undefined
|
||||
groups: Float32Array | Uint8Array
|
||||
vertexCount: number
|
||||
drawCount: number
|
||||
} | undefined
|
||||
meshes: Mesh[] | undefined
|
||||
values: BaseValues
|
||||
values: BaseValues & { readonly uDoubleSided?: ValueCell<any> }
|
||||
isGeoTexture: boolean
|
||||
mode: MeshMode
|
||||
webgl: WebGLContext | undefined
|
||||
ctx: RuntimeContext
|
||||
}
|
||||
@@ -55,7 +63,8 @@ export type MeshGeoData = {
|
||||
groups: Float32Array | Uint8Array,
|
||||
vertexCount: number,
|
||||
instanceIndex: number,
|
||||
isGeoTexture: boolean
|
||||
isGeoTexture: boolean,
|
||||
mode: MeshMode
|
||||
}
|
||||
|
||||
export abstract class MeshExporter<D extends RenderObjectExportData> implements RenderObjectExporter<D> {
|
||||
@@ -222,7 +231,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
|
||||
protected static getColor(vertexIndex: number, geoData: MeshGeoData, interpolatedColors?: Uint8Array, interpolatedOverpaint?: Uint8Array): Color {
|
||||
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const colorType = values.dColorType.ref.value;
|
||||
const uColor = values.uColor.ref.value;
|
||||
@@ -231,6 +240,12 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const dOverpaint = values.dOverpaint.ref.value;
|
||||
const tOverpaint = values.tOverpaint.ref.value.array;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
let color: Color;
|
||||
switch (colorType) {
|
||||
case 'uniform':
|
||||
@@ -298,12 +313,18 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
|
||||
protected static getTransparency(vertexIndex: number, geoData: MeshGeoData, interpolatedTransparency?: Uint8Array): number {
|
||||
const { values, instanceIndex, isGeoTexture, groups, vertexCount } = geoData;
|
||||
const { values, instanceIndex, isGeoTexture, mode, groups } = geoData;
|
||||
const groupCount = values.uGroupCount.ref.value;
|
||||
const dTransparency = values.dTransparency.ref.value;
|
||||
const tTransparency = values.tTransparency.ref.value.array;
|
||||
const transparencyType = values.dTransparencyType.ref.value;
|
||||
|
||||
let vertexCount = geoData.vertexCount;
|
||||
if (mode === 'lines') {
|
||||
vertexIndex *= 2;
|
||||
vertexCount *= 2;
|
||||
}
|
||||
|
||||
let transparency: number = 0;
|
||||
if (dTransparency) {
|
||||
switch (transparencyType) {
|
||||
@@ -329,7 +350,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
return transparency;
|
||||
}
|
||||
|
||||
protected abstract addMeshWithColors(input: AddMeshInput): void;
|
||||
protected abstract addMeshWithColors(input: AddMeshInput): Promise<void>;
|
||||
|
||||
private async addMesh(values: MeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
@@ -349,36 +370,132 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
drawCount = values.drawCount.ref.value;
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: aNormal, indices, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addLines(values: LinesValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const vertexCount = (values.uVertexCount.ref.value / 4) * 2;
|
||||
const drawCount = values.drawCount.ref.value / (2 * 3);
|
||||
|
||||
if (this.options.linesAsTriangles) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const radialSegments = 6;
|
||||
const topCap = true;
|
||||
const bottomCap = true;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0, il = vertexCount * 2; i < il; i += 4) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
} else {
|
||||
const n = vertexCount / 2;
|
||||
const vertices = new Float32Array(n * 2 * 3);
|
||||
for (let i = 0; i < n; ++i) {
|
||||
vertices[i * 6] = aStart[i * 4 * 3];
|
||||
vertices[i * 6 + 1] = aStart[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 2] = aStart[i * 4 * 3 + 2];
|
||||
|
||||
vertices[i * 6 + 3] = aEnd[i * 4 * 3];
|
||||
vertices[i * 6 + 4] = aEnd[i * 4 * 3 + 1];
|
||||
vertices[i * 6 + 5] = aEnd[i * 4 * 3 + 2];
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'lines', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addPoints(values: PointsValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
// TODO
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
if (this.options.pointsAsTriangles) {
|
||||
const center = Vec3();
|
||||
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const detail = 0;
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * 0.03;
|
||||
state.currentGroup = group;
|
||||
addSphere(state, center, radius, detail);
|
||||
}
|
||||
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
} else {
|
||||
await this.addMeshWithColors({ mesh: { vertices: aPosition, normals: undefined, indices: undefined, groups: aGroup, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: false, mode: 'points', webgl, ctx });
|
||||
}
|
||||
}
|
||||
|
||||
private async addSpheres(values: SpheresValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const center = Vec3();
|
||||
|
||||
const aPosition = values.aPosition.ref.value;
|
||||
const aGroup = values.aGroup.ref.value;
|
||||
const aPosition = values.centerBuffer.ref.value;
|
||||
const aGroup = values.groupBuffer.ref.value;
|
||||
const instanceCount = values.instanceCount.ref.value;
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const meshes: Mesh[] = [];
|
||||
|
||||
const sphereCount = vertexCount / 4 * instanceCount;
|
||||
const sphereCount = vertexCount / 6 * instanceCount;
|
||||
let detail: number;
|
||||
if (sphereCount < 2000) detail = 3;
|
||||
else if (sphereCount < 20000) detail = 2;
|
||||
else detail = 1;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
if (sphereCount < 2000) detail = 3;
|
||||
else if (sphereCount < 20000) detail = 2;
|
||||
else detail = 1;
|
||||
break;
|
||||
case 'high':
|
||||
detail = 3;
|
||||
break;
|
||||
case 'medium':
|
||||
detail = 2;
|
||||
break;
|
||||
case 'low':
|
||||
detail = 1;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
|
||||
for (let i = 0; i < vertexCount; i += 4) {
|
||||
for (let i = 0; i < sphereCount; ++i) {
|
||||
v3fromArray(center, aPosition, i * 3);
|
||||
|
||||
const group = aGroup[i];
|
||||
@@ -390,12 +507,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addCylinders(values: CylindersValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
const start = Vec3();
|
||||
const end = Vec3();
|
||||
const dir = Vec3();
|
||||
|
||||
const aStart = values.aStart.ref.value;
|
||||
const aEnd = values.aEnd.ref.value;
|
||||
@@ -408,9 +526,24 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
|
||||
const cylinderCount = vertexCount / 6 * instanceCount;
|
||||
let radialSegments: number;
|
||||
if (cylinderCount < 2000) radialSegments = 36;
|
||||
else if (cylinderCount < 20000) radialSegments = 24;
|
||||
else radialSegments = 12;
|
||||
switch (this.options.primitivesQuality) {
|
||||
case 'auto':
|
||||
if (cylinderCount < 2000) radialSegments = 36;
|
||||
else if (cylinderCount < 20000) radialSegments = 24;
|
||||
else radialSegments = 12;
|
||||
break;
|
||||
case 'high':
|
||||
radialSegments = 36;
|
||||
break;
|
||||
case 'medium':
|
||||
radialSegments = 24;
|
||||
break;
|
||||
case 'low':
|
||||
radialSegments = 12;
|
||||
break;
|
||||
default:
|
||||
assertUnreachable(this.options.primitivesQuality);
|
||||
}
|
||||
|
||||
for (let instanceIndex = 0; instanceIndex < instanceCount; ++instanceIndex) {
|
||||
const state = MeshBuilder.createState(512, 256);
|
||||
@@ -418,13 +551,17 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
for (let i = 0; i < vertexCount; i += 6) {
|
||||
v3fromArray(start, aStart, i * 3);
|
||||
v3fromArray(end, aEnd, i * 3);
|
||||
v3sub(dir, end, start);
|
||||
|
||||
const group = aGroup[i];
|
||||
const radius = MeshExporter.getSize(values, instanceIndex, group) * aScale[i];
|
||||
const cap = aCap[i];
|
||||
const topCap = cap === 1 || cap === 3;
|
||||
const bottomCap = cap >= 2;
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
let topCap = cap === 1 || cap === 3;
|
||||
let bottomCap = cap >= 2;
|
||||
if (v3dot(v3unitY, dir) > 0) {
|
||||
[bottomCap, topCap] = [topCap, bottomCap];
|
||||
}
|
||||
const cylinderProps = { radiusTop: radius, radiusBottom: radius, topCap, bottomCap, radialSegments };
|
||||
state.currentGroup = aGroup[i];
|
||||
addCylinder(state, start, end, 1, cylinderProps);
|
||||
}
|
||||
@@ -432,7 +569,7 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
meshes.push(MeshBuilder.getMesh(state));
|
||||
}
|
||||
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: undefined, meshes, values, isGeoTexture: false, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
private async addTextureMesh(values: TextureMeshValues, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
@@ -457,11 +594,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
const vertexCount = values.uVertexCount.ref.value;
|
||||
const drawCount = values.drawCount.ref.value;
|
||||
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, webgl, ctx });
|
||||
await this.addMeshWithColors({ mesh: { vertices, normals, indices: undefined, groups, vertexCount, drawCount }, meshes: undefined, values, isGeoTexture: true, mode: 'triangles', webgl, ctx });
|
||||
}
|
||||
|
||||
add(renderObject: GraphicsRenderObject, webgl: WebGLContext, ctx: RuntimeContext) {
|
||||
if (!renderObject.state.visible) return;
|
||||
if (!renderObject.state.visible && !this.options.includeHidden) return;
|
||||
if (renderObject.values.drawCount.ref.value === 0) return;
|
||||
if (renderObject.values.instanceCount.ref.value === 0) return;
|
||||
|
||||
switch (renderObject.type) {
|
||||
case 'mesh':
|
||||
@@ -479,6 +618,13 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
|
||||
}
|
||||
}
|
||||
|
||||
protected options = {
|
||||
includeHidden: false,
|
||||
linesAsTriangles: false,
|
||||
pointsAsTriangles: false,
|
||||
primitivesQuality: 'auto' as 'auto' | 'high' | 'medium' | 'low',
|
||||
};
|
||||
|
||||
abstract getData(ctx: RuntimeContext): Promise<D>;
|
||||
|
||||
abstract getBlob(ctx: RuntimeContext): Promise<Blob>;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -70,7 +70,8 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const obj = this.obj;
|
||||
const t = Mat4();
|
||||
@@ -86,19 +87,19 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
const instanceCount = values.uInstanceCount.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = ObjExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
interpolatedColors = ObjExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
interpolatedOverpaint = ObjExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = ObjExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
@@ -126,7 +127,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
|
||||
StringBuilder.writeSafe(obj, 'vn ');
|
||||
StringBuilder.writeFloat(obj, tmpV[0], 100);
|
||||
StringBuilder.whitespace1(obj);
|
||||
@@ -136,7 +137,7 @@ export class ObjExporter extends MeshExporter<ObjData> {
|
||||
StringBuilder.newline(obj);
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
|
||||
// color
|
||||
const quantizedColors = new Uint8Array(drawCount * 3);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
*/
|
||||
@@ -30,7 +30,8 @@ export class StlExporter extends MeshExporter<StlData> {
|
||||
private centerTransform: Mat4;
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { values, isGeoTexture, ctx } = input;
|
||||
const { values, isGeoTexture, mode, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const t = Mat4();
|
||||
const tmpV = Vec3();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Sukolsak Sakshuwong <sukolsak@stanford.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -61,7 +61,8 @@ def Material "material${materialKey}"
|
||||
}
|
||||
|
||||
protected async addMeshWithColors(input: AddMeshInput) {
|
||||
const { mesh, values, isGeoTexture, webgl, ctx } = input;
|
||||
const { mesh, values, isGeoTexture, mode, webgl, ctx } = input;
|
||||
if (mode !== 'triangles') return;
|
||||
|
||||
const t = Mat4();
|
||||
const n = Mat3();
|
||||
@@ -78,20 +79,20 @@ def Material "material${materialKey}"
|
||||
const roughness = values.uRoughness.ref.value;
|
||||
|
||||
let interpolatedColors: Uint8Array | undefined;
|
||||
if (colorType === 'volume' || colorType === 'volumeInstance') {
|
||||
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType });
|
||||
if (webgl && mesh && (colorType === 'volume' || colorType === 'volumeInstance')) {
|
||||
interpolatedColors = UsdzExporter.getInterpolatedColors(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType });
|
||||
}
|
||||
|
||||
let interpolatedOverpaint: Uint8Array | undefined;
|
||||
if (overpaintType === 'volumeInstance') {
|
||||
if (webgl && mesh && overpaintType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: overpaintType });
|
||||
interpolatedOverpaint = UsdzExporter.getInterpolatedOverpaint(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: overpaintType });
|
||||
}
|
||||
|
||||
let interpolatedTransparency: Uint8Array | undefined;
|
||||
if (transparencyType === 'volumeInstance') {
|
||||
if (webgl && mesh && transparencyType === 'volumeInstance') {
|
||||
const stride = isGeoTexture ? 4 : 3;
|
||||
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl!, { vertices: mesh!.vertices, vertexCount: mesh!.vertexCount, values, stride, colorType: transparencyType });
|
||||
interpolatedTransparency = UsdzExporter.getInterpolatedTransparency(webgl, { vertices: mesh.vertices, vertexCount: mesh.vertexCount, values, stride, colorType: transparencyType });
|
||||
}
|
||||
|
||||
await ctx.update({ isIndeterminate: false, current: 0, max: instanceCount });
|
||||
@@ -123,7 +124,7 @@ def Material "material${materialKey}"
|
||||
|
||||
// normal
|
||||
for (let i = 0; i < vertexCount; ++i) {
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals, i * stride), n);
|
||||
v3transformMat3(tmpV, v3fromArray(tmpV, normals!, i * stride), n);
|
||||
StringBuilder.writeSafe(normalBuilder, (i === 0) ? '(' : ',(');
|
||||
StringBuilder.writeFloat(normalBuilder, tmpV[0], 100);
|
||||
StringBuilder.writeSafe(normalBuilder, ',');
|
||||
@@ -133,7 +134,7 @@ def Material "material${materialKey}"
|
||||
StringBuilder.writeSafe(normalBuilder, ')');
|
||||
}
|
||||
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture };
|
||||
const geoData = { values, groups, vertexCount, instanceIndex, isGeoTexture, mode };
|
||||
|
||||
// face
|
||||
for (let i = 0; i < drawCount; ++i) {
|
||||
|
||||
@@ -17,9 +17,9 @@ import { UUID } from '../../../mol-util';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from '../../volumes-and-segmentations/helpers';
|
||||
import { MetadataWrapper } from '../../volumes-and-segmentations/volseg-api/utils';
|
||||
|
||||
import { MeshlistData } from '../mesh-extension';
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from '../../volumes-and-segmentations/helpers';
|
||||
|
||||
|
||||
export const DEFAULT_MESH_SERVER = 'http://localhost:9000/v2';
|
||||
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
|
||||
import { to_mmCIF, Unit } from '../../mol-model/structure';
|
||||
import { Structure, to_mmCIF, Unit } from '../../mol-model/structure';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { Task } from '../../mol-task';
|
||||
import { getFormattedTime } from '../../mol-util/date';
|
||||
import { download } from '../../mol-util/download';
|
||||
import { zip } from '../../mol-util/zip/zip';
|
||||
|
||||
const ModelExportNameProp = '__ModelExportName__';
|
||||
export const ModelExport = {
|
||||
getStructureName(structure: Structure): string | undefined {
|
||||
return structure.inheritedPropertyData[ModelExportNameProp];
|
||||
},
|
||||
setStructureName(structure: Structure, name: string) {
|
||||
return structure.inheritedPropertyData[ModelExportNameProp] = name;
|
||||
}
|
||||
};
|
||||
|
||||
export async function exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'bcif' }) {
|
||||
try {
|
||||
await plugin.runTask(_exportHierarchy(plugin, options), { useOverlay: true });
|
||||
@@ -43,19 +54,21 @@ function _exportHierarchy(plugin: PluginContext, options?: { format?: 'cif' | 'b
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = entryMap.has(s.model.entryId)
|
||||
? `${s.model.entryId}_${entryMap.get(s.model.entryId)! + 1}.${format}`
|
||||
: `${s.model.entryId}.${format}`;
|
||||
entryMap.set(s.model.entryId, (entryMap.get(s.model.entryId) ?? 0) + 1);
|
||||
const name = ModelExport.getStructureName(s) || s.model.entryId || 'unnamed';
|
||||
|
||||
await ctx.update({ message: `Exporting ${s.model.entryId}...`, isIndeterminate: true, canAbort: false });
|
||||
const fileName = entryMap.has(name)
|
||||
? `${name}_${entryMap.get(name)! + 1}.${format}`
|
||||
: `${name}.${format}`;
|
||||
entryMap.set(name, (entryMap.get(name) ?? 0) + 1);
|
||||
|
||||
await ctx.update({ message: `Exporting ${name}...`, isIndeterminate: true, canAbort: false });
|
||||
if (s.elementCount > 100000) {
|
||||
// Give UI chance to update, only needed for larger structures.
|
||||
await new Promise(res => setTimeout(res, 50));
|
||||
}
|
||||
|
||||
try {
|
||||
files.push([name, to_mmCIF(s.model.entryId, s, format === 'bcif', { copyAllCategories: true })]);
|
||||
files.push([fileName, to_mmCIF(name, s, format === 'bcif', { copyAllCategories: true })]);
|
||||
} catch (e) {
|
||||
if (format === 'cif' && s.elementCount > 2000000) {
|
||||
plugin.log.warn(`[Export] The structure might be too big to be exported as Text CIF, consider using the BinaryCIF format instead.`);
|
||||
|
||||
1
src/extensions/mvs/README.md
Normal file
1
src/extensions/mvs/README.md
Normal file
@@ -0,0 +1 @@
|
||||
Find the MVS extension documentation [here](../../../docs/extensions/mvs/README.md).
|
||||
46
src/extensions/mvs/_spec/mvs-data.spec.ts
Normal file
46
src/extensions/mvs/_spec/mvs-data.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import { MVSData } from '../mvs-data';
|
||||
|
||||
|
||||
describe('MVSData', () => {
|
||||
it('MVSData functions work', async () => {
|
||||
const data = fs.readFileSync('examples/mvs/1cbs.mvsj', { encoding: 'utf8' });
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
expect(mvsData).toBeTruthy();
|
||||
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
|
||||
expect(MVSData.isValid(mvsData)).toEqual(true);
|
||||
|
||||
const reencoded = MVSData.toMVSJ(mvsData);
|
||||
expect(reencoded.replace(/\s/g, '')).toEqual(data.replace(/\s/g, ''));
|
||||
|
||||
const prettyString = MVSData.toPrettyString(mvsData);
|
||||
expect(typeof prettyString).toEqual('string');
|
||||
expect(prettyString.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('MVSData builder works', async () => {
|
||||
const builder = MVSData.createBuilder();
|
||||
expect(builder).toBeTruthy();
|
||||
|
||||
const mvsData = builder.getState();
|
||||
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
|
||||
|
||||
builder
|
||||
.download({ url: 'http://example.com' })
|
||||
.parse({ format: 'mmcif' })
|
||||
.assemblyStructure({ assembly_id: '1' })
|
||||
.component({ selector: 'polymer' })
|
||||
.representation()
|
||||
.color({ color: 'green', selector: { label_asym_id: 'A' } });
|
||||
const mvsData2 = builder.getState();
|
||||
expect(MVSData.validationIssues(mvsData2)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
176
src/extensions/mvs/behavior.ts
Normal file
176
src/extensions/mvs/behavior.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { CustomModelProperty } from '../../mol-model-props/common/custom-model-property';
|
||||
import { CustomStructureProperty } from '../../mol-model-props/common/custom-structure-property';
|
||||
import { DataFormatProvider } from '../../mol-plugin-state/formats/provider';
|
||||
import { PluginDragAndDropHandler } from '../../mol-plugin-state/manager/drag-and-drop';
|
||||
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StructureRepresentationProvider } from '../../mol-repr/structure/representation';
|
||||
import { StateAction } from '../../mol-state';
|
||||
import { ColorTheme } from '../../mol-theme/color';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { MVSAnnotationColorThemeProvider } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationTooltipsLabelProvider, MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
import { CustomLabelRepresentationProvider } from './components/custom-label/representation';
|
||||
import { CustomTooltipsLabelProvider, CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { LoadMvsData, MVSJFormatProvider } from './components/formats';
|
||||
import { makeMultilayerColorThemeProvider } from './components/multilayer-color-theme';
|
||||
import { loadMVS } from './load';
|
||||
import { MVSData } from './mvs-data';
|
||||
|
||||
|
||||
/** Collection of things that can be register/unregistered in a plugin */
|
||||
interface Registrables {
|
||||
customModelProperties?: CustomModelProperty.Provider<any, any>[],
|
||||
customStructureProperties?: CustomStructureProperty.Provider<any, any>[],
|
||||
representations?: StructureRepresentationProvider<any>[],
|
||||
colorThemes?: ColorTheme.Provider[],
|
||||
lociLabels?: LociLabelProvider[],
|
||||
dragAndDropHandlers?: DragAndDropHandler[],
|
||||
dataFormats?: { name: string, provider: DataFormatProvider }[],
|
||||
actions?: StateAction[],
|
||||
}
|
||||
|
||||
|
||||
/** Registers everything needed for loading MolViewSpec files */
|
||||
export const MolViewSpec = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'molviewspec',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'MolViewSpec',
|
||||
description: 'MolViewSpec extension',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
private readonly registrables: Registrables = {
|
||||
customModelProperties: [
|
||||
MVSAnnotationsProvider,
|
||||
],
|
||||
customStructureProperties: [
|
||||
CustomTooltipsProvider,
|
||||
MVSAnnotationTooltipsProvider,
|
||||
],
|
||||
representations: [
|
||||
CustomLabelRepresentationProvider,
|
||||
MVSAnnotationLabelRepresentationProvider,
|
||||
],
|
||||
colorThemes: [
|
||||
MVSAnnotationColorThemeProvider,
|
||||
makeMultilayerColorThemeProvider(this.ctx.representation.structure.themes.colorThemeRegistry),
|
||||
],
|
||||
lociLabels: [
|
||||
CustomTooltipsLabelProvider,
|
||||
MVSAnnotationTooltipsLabelProvider,
|
||||
],
|
||||
dragAndDropHandlers: [
|
||||
MVSDragAndDropHandler,
|
||||
],
|
||||
dataFormats: [
|
||||
{ name: 'MVSJ', provider: MVSJFormatProvider },
|
||||
],
|
||||
actions: [
|
||||
LoadMvsData,
|
||||
]
|
||||
};
|
||||
|
||||
register(): void {
|
||||
for (const prop of this.registrables.customModelProperties ?? []) {
|
||||
this.ctx.customModelProperties.register(prop, this.params.autoAttach);
|
||||
}
|
||||
for (const prop of this.registrables.customStructureProperties ?? []) {
|
||||
this.ctx.customStructureProperties.register(prop, this.params.autoAttach);
|
||||
}
|
||||
for (const repr of this.registrables.representations ?? []) {
|
||||
this.ctx.representation.structure.registry.add(repr);
|
||||
}
|
||||
for (const theme of this.registrables.colorThemes ?? []) {
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.add(theme);
|
||||
}
|
||||
for (const provider of this.registrables.lociLabels ?? []) {
|
||||
this.ctx.managers.lociLabels.addProvider(provider);
|
||||
}
|
||||
for (const handler of this.registrables.dragAndDropHandlers ?? []) {
|
||||
this.ctx.managers.dragAndDrop.addHandler(handler.name, handler.handle);
|
||||
}
|
||||
for (const format of this.registrables.dataFormats ?? []) {
|
||||
this.ctx.dataFormats.add(format.name, format.provider);
|
||||
}
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.add(action);
|
||||
}
|
||||
}
|
||||
update(p: { autoAttach: boolean }) {
|
||||
const updated = this.params.autoAttach !== p.autoAttach;
|
||||
this.params.autoAttach = p.autoAttach;
|
||||
for (const prop of this.registrables.customModelProperties ?? []) {
|
||||
this.ctx.customModelProperties.setDefaultAutoAttach(prop.descriptor.name, this.params.autoAttach);
|
||||
}
|
||||
for (const prop of this.registrables.customStructureProperties ?? []) {
|
||||
this.ctx.customStructureProperties.setDefaultAutoAttach(prop.descriptor.name, this.params.autoAttach);
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
unregister() {
|
||||
for (const prop of this.registrables.customModelProperties ?? []) {
|
||||
this.ctx.customModelProperties.unregister(prop.descriptor.name);
|
||||
}
|
||||
for (const prop of this.registrables.customStructureProperties ?? []) {
|
||||
this.ctx.customStructureProperties.unregister(prop.descriptor.name);
|
||||
}
|
||||
for (const repr of this.registrables.representations ?? []) {
|
||||
this.ctx.representation.structure.registry.remove(repr);
|
||||
}
|
||||
for (const theme of this.registrables.colorThemes ?? []) {
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.remove(theme);
|
||||
}
|
||||
for (const labelProvider of this.registrables.lociLabels ?? []) {
|
||||
this.ctx.managers.lociLabels.removeProvider(labelProvider);
|
||||
}
|
||||
for (const handler of this.registrables.dragAndDropHandlers ?? []) {
|
||||
this.ctx.managers.dragAndDrop.removeHandler(handler.name);
|
||||
}
|
||||
for (const format of this.registrables.dataFormats ?? []) {
|
||||
this.ctx.dataFormats.remove(format.name);
|
||||
}
|
||||
for (const action of this.registrables.actions ?? []) {
|
||||
this.ctx.state.data.actions.remove(action);
|
||||
}
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(false),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
/** Registrable method for handling dragged-and-dropped files */
|
||||
interface DragAndDropHandler {
|
||||
name: string,
|
||||
handle: PluginDragAndDropHandler,
|
||||
}
|
||||
|
||||
/** DragAndDropHandler handler for `.mvsj` files */
|
||||
const MVSDragAndDropHandler: DragAndDropHandler = {
|
||||
name: 'mvs-mvsj',
|
||||
/** Load .mvsj files. Delete previous plugin state before loading.
|
||||
* If multiple files are provided, merge their MVS data into one state. */
|
||||
async handle(files: File[], plugin: PluginContext): Promise<boolean> {
|
||||
let applied = false;
|
||||
for (const file of files) {
|
||||
if (file.name.toLowerCase().endsWith('.mvsj')) {
|
||||
const data = await file.text();
|
||||
const mvsData = MVSData.fromMVSJ(data);
|
||||
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: !applied, sourceUrl: undefined });
|
||||
applied = true;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
},
|
||||
};
|
||||
144
src/extensions/mvs/camera.ts
Normal file
144
src/extensions/mvs/camera.ts
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Camera } from '../../mol-canvas3d/camera';
|
||||
import { GraphicsRenderObject } from '../../mol-gl/render-object';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { Loci } from '../../mol-model/loci';
|
||||
import { Structure } from '../../mol-model/structure';
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { ColorNames } from '../../mol-util/color/names';
|
||||
|
||||
import { decodeColor } from './helpers/utils';
|
||||
import { ParamsOfKind } from './tree/generic/tree-schema';
|
||||
import { MolstarTree } from './tree/molstar/molstar-tree';
|
||||
import { MVSDefaults } from './tree/mvs/mvs-defaults';
|
||||
|
||||
|
||||
const DefaultFocusOptions = {
|
||||
minRadius: 5,
|
||||
extraRadiusForFocus: 0,
|
||||
extraRadiusForZoomAll: 0,
|
||||
};
|
||||
const DefaultCanvasBackgroundColor = ColorNames.white;
|
||||
|
||||
|
||||
const _tmpVec = Vec3();
|
||||
|
||||
/** Set the camera based on a camera node params. */
|
||||
export async function setCamera(plugin: PluginContext, params: ParamsOfKind<MolstarTree, 'camera'>) {
|
||||
const target = Vec3.create(...params.target);
|
||||
let position = Vec3.create(...params.position);
|
||||
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
|
||||
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius: 10_000, 'radiusMax': 10_000 };
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
|
||||
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is null).
|
||||
* Orient the camera based on a focus node params. */
|
||||
export async function setFocus(plugin: PluginContext, structureNodeSelector: StateObjectSelector | undefined, params: ParamsOfKind<MolstarTree, 'focus'> = MVSDefaults.focus) {
|
||||
let structure: Structure | undefined = undefined;
|
||||
if (structureNodeSelector) {
|
||||
const cell = plugin.state.data.cells.get(structureNodeSelector.ref);
|
||||
structure = cell?.obj?.data;
|
||||
if (!structure) console.warn('Focus: no structure');
|
||||
if (!(structure instanceof Structure)) {
|
||||
console.warn('Focus: cannot apply to a non-structure node');
|
||||
structure = undefined;
|
||||
}
|
||||
}
|
||||
const boundingSphere = structure ? Loci.getBoundingSphere(Structure.Loci(structure)) : getPluginBoundingSphere(plugin);
|
||||
if (boundingSphere && plugin.canvas3d) {
|
||||
const extraRadius = structure ? DefaultFocusOptions.extraRadiusForFocus : DefaultFocusOptions.extraRadiusForZoomAll;
|
||||
const direction = Vec3.create(...params.direction);
|
||||
const up = Vec3.create(...params.up);
|
||||
Vec3.orthogonalize(up, direction, up);
|
||||
const snapshot = snapshotFromSphereAndDirections(plugin.canvas3d.camera, {
|
||||
center: boundingSphere.center,
|
||||
radius: boundingSphere.radius + extraRadius,
|
||||
up,
|
||||
direction,
|
||||
});
|
||||
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
|
||||
}
|
||||
}
|
||||
|
||||
/** Return camera snapshot for focusing a sphere with given `center` and `radius`,
|
||||
* while ensuring given view `direction` (aligns with vector position->target)
|
||||
* and `up` (aligns with screen Y axis). */
|
||||
function snapshotFromSphereAndDirections(camera: Camera, options: { center: Vec3, radius: number, direction: Vec3, up: Vec3 }): Partial<Camera.Snapshot> {
|
||||
// This might seem to repeat `plugin.canvas3d.camera.getFocus` but avoid flipping
|
||||
const { center, direction, up } = options;
|
||||
const radius = Math.max(options.radius, DefaultFocusOptions.minRadius);
|
||||
const distance = camera.getTargetDistance(radius);
|
||||
const deltaDirection = Vec3.setMagnitude(_tmpVec, direction, distance);
|
||||
const position = Vec3.sub(Vec3(), center, deltaDirection);
|
||||
return { target: center, position, up, radius };
|
||||
}
|
||||
|
||||
/** Return the distance adjustment ratio for conversion from the "reference camera"
|
||||
* to a camera with an arbitrary field of view `fov`. */
|
||||
function distanceAdjustment(mode: Camera.Mode, fov: number) {
|
||||
if (mode === 'orthographic') return 1 / (2 * Math.tan(fov / 2));
|
||||
else return 1 / (2 * Math.sin(fov / 2));
|
||||
}
|
||||
|
||||
/** Return the position for a camera with an arbitrary field of view `fov`
|
||||
* necessary to just fit into view the same sphere (with center at `target`)
|
||||
* as the "reference camera" placed at `refPosition` would fit, while keeping the camera orientation.
|
||||
* The "reference camera" is a camera which can just fit into view a sphere of radius R with center at distance 2R
|
||||
* (this corresponds to FOV = 2 * asin(1/2) in perspective mode or FOV = 2 * atan(1/2) in orthographic mode). */
|
||||
function fovAdjustedPosition(target: Vec3, refPosition: Vec3, mode: Camera.Mode, fov: number) {
|
||||
const delta = Vec3.sub(Vec3(), refPosition, target);
|
||||
const adjustment = distanceAdjustment(mode, fov);
|
||||
return Vec3.scaleAndAdd(delta, target, delta, adjustment); // return target + delta * adjustment
|
||||
}
|
||||
|
||||
/** Compute the bounding sphere of the whole scene. */
|
||||
function getPluginBoundingSphere(plugin: PluginContext) {
|
||||
const renderObjects = getRenderObjects(plugin, false);
|
||||
const spheres = renderObjects.map(r => r.values.boundingSphere.ref.value).filter(sphere => sphere.radius > 0);
|
||||
return boundingSphereOfSpheres(spheres);
|
||||
}
|
||||
|
||||
function getRenderObjects(plugin: PluginContext, includeHidden: boolean): GraphicsRenderObject[] {
|
||||
let reprCells = Array.from(plugin.state.data.cells.values()).filter(cell => cell.obj && PluginStateObject.isRepresentation3D(cell.obj));
|
||||
if (!includeHidden) reprCells = reprCells.filter(cell => !cell.state.isHidden);
|
||||
const renderables = reprCells.flatMap(cell => cell.obj!.data.repr.renderObjects);
|
||||
return renderables;
|
||||
}
|
||||
|
||||
let boundaryHelper: BoundaryHelper | undefined = undefined;
|
||||
|
||||
function boundingSphereOfSpheres(spheres: Sphere3D[]): Sphere3D {
|
||||
boundaryHelper ??= new BoundaryHelper('98');
|
||||
boundaryHelper.reset();
|
||||
for (const s of spheres) boundaryHelper.includeSphere(s);
|
||||
boundaryHelper.finishedIncludeStep();
|
||||
for (const s of spheres) boundaryHelper.radiusSphere(s);
|
||||
return boundaryHelper.getSphere();
|
||||
}
|
||||
|
||||
/** Set canvas properties based on a canvas node params. */
|
||||
export function setCanvas(plugin: PluginContext, params: ParamsOfKind<MolstarTree, 'canvas'> | undefined) {
|
||||
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
|
||||
if (backgroundColor !== plugin.canvas3d?.props.renderer.backgroundColor) {
|
||||
plugin.canvas3d?.setProps(old => ({
|
||||
...old,
|
||||
renderer: {
|
||||
...old.renderer,
|
||||
backgroundColor: backgroundColor,
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
80
src/extensions/mvs/components/annotation-color-theme.ts
Normal file
80
src/extensions/mvs/components/annotation-color-theme.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { decodeColor } from '../helpers/utils';
|
||||
import { getMVSAnnotationForStructure } from './annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for color theme "MVS Annotation" */
|
||||
export const MVSAnnotationColorThemeParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property' }),
|
||||
fieldName: PD.Text('color', { description: 'Annotation field (column) from which to take color values' }),
|
||||
background: PD.Color(ColorNames.gainsboro, { description: 'Color for elements without annotation' }),
|
||||
};
|
||||
export type MVSAnnotationColorThemeParams = typeof MVSAnnotationColorThemeParams
|
||||
|
||||
/** Parameter values for color theme "MVS Annotation" */
|
||||
export type MVSAnnotationColorThemeProps = PD.Values<MVSAnnotationColorThemeParams>
|
||||
|
||||
|
||||
/** Return color theme that assigns colors based on an annotation file.
|
||||
* The annotation file itself is handled by a custom model property (`MVSAnnotationsProvider`),
|
||||
* the color theme then just uses this property. */
|
||||
export function MVSAnnotationColorTheme(ctx: ThemeDataContext, props: MVSAnnotationColorThemeProps): ColorTheme<MVSAnnotationColorThemeParams> {
|
||||
let color: LocationColor = () => props.background;
|
||||
|
||||
if (ctx.structure && !ctx.structure.isEmpty) {
|
||||
const { annotation } = getMVSAnnotationForStructure(ctx.structure, props.annotationId);
|
||||
if (annotation) {
|
||||
const colorForStructureElementLocation = (location: StructureElement.Location) => {
|
||||
// if (annot.getAnnotationForLocation(location)?.color !== annot.getAnnotationForLocation_Reference(location)?.color) throw new Error('AssertionError');
|
||||
return decodeColor(annotation?.getValueForLocation(location, props.fieldName)) ?? props.background;
|
||||
};
|
||||
const auxLocation = StructureElement.Location.create(ctx.structure);
|
||||
|
||||
color = (location: Location) => {
|
||||
if (StructureElement.Location.is(location)) {
|
||||
return colorForStructureElementLocation(location);
|
||||
} else if (Bond.isLocation(location)) {
|
||||
// this will be applied for each bond twice, to get color of each half (a* refers to the adjacent atom, b* to the opposite atom)
|
||||
auxLocation.unit = location.aUnit;
|
||||
auxLocation.element = location.aUnit.elements[location.aIndex];
|
||||
return colorForStructureElementLocation(auxLocation);
|
||||
}
|
||||
return props.background;
|
||||
};
|
||||
} else {
|
||||
console.error(`Annotation source "${props.annotationId}" not present`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
factory: MVSAnnotationColorTheme,
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Assigns colors based on custom MolViewSpec annotation data.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** A thingy that is needed to register color theme "MVS Annotation" */
|
||||
export const MVSAnnotationColorThemeProvider: ColorTheme.Provider<MVSAnnotationColorThemeParams, 'mvs-annotation'> = {
|
||||
name: 'mvs-annotation',
|
||||
label: 'MVS Annotation',
|
||||
category: ColorTheme.Category.Misc,
|
||||
factory: MVSAnnotationColorTheme,
|
||||
getParams: ctx => MVSAnnotationColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(MVSAnnotationColorThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) => true,
|
||||
};
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../../mol-repr/representation';
|
||||
import { ComplexRepresentation, StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../../../../mol-repr/structure/representation';
|
||||
import { MarkerAction } from '../../../../mol-util/marker-action';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { MVSAnnotationLabelTextParams, MVSAnnotationLabelTextVisual } from './visual';
|
||||
|
||||
|
||||
/** Components of "MVS Annotation Label" representation */
|
||||
const MVSAnnotationLabelVisuals = {
|
||||
'label-text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MVSAnnotationLabelTextParams>) => ComplexRepresentation('Label text', ctx, getParams, MVSAnnotationLabelTextVisual),
|
||||
};
|
||||
|
||||
/** Parameter definition for representation type "MVS Annotation Label" */
|
||||
export type MVSAnnotationLabelParams = typeof MVSAnnotationLabelParams
|
||||
export const MVSAnnotationLabelParams = {
|
||||
...MVSAnnotationLabelTextParams,
|
||||
visuals: PD.MultiSelect(['label-text'], PD.objectToOptions(MVSAnnotationLabelVisuals)),
|
||||
};
|
||||
|
||||
/** Parameter values for representation type "MVS Annotation Label" */
|
||||
export type MVSAnnotationLabelProps = PD.ValuesFor<MVSAnnotationLabelParams>
|
||||
|
||||
/** Structure representation type "MVS Annotation Label", allowing showing labels based on "MVS Annotations" custom props */
|
||||
export type MVSAnnotationLabelRepresentation = StructureRepresentation<MVSAnnotationLabelParams>
|
||||
export function MVSAnnotationLabelRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, MVSAnnotationLabelParams>): MVSAnnotationLabelRepresentation {
|
||||
const repr = Representation.createMulti('Label', ctx, getParams, StructureRepresentationStateBuilder, MVSAnnotationLabelVisuals as unknown as Representation.Def<Structure, MVSAnnotationLabelParams>);
|
||||
repr.setState({ pickable: false, markerActions: MarkerAction.None });
|
||||
return repr;
|
||||
}
|
||||
|
||||
/** A thingy that is needed to register representation type "MVS Annotation Label", allowing showing labels based on "MVS Annotations" custom props */
|
||||
export const MVSAnnotationLabelRepresentationProvider = StructureRepresentationProvider({
|
||||
name: 'mvs-annotation-label',
|
||||
label: 'MVS Annotation Label',
|
||||
description: 'Displays labels based on annotation custom model property',
|
||||
factory: MVSAnnotationLabelRepresentation,
|
||||
getParams: () => MVSAnnotationLabelParams,
|
||||
defaultValues: PD.getDefaultValues(MVSAnnotationLabelParams),
|
||||
defaultColorTheme: { name: 'uniform' }, // this ain't workin
|
||||
defaultSizeTheme: { name: 'physical' },
|
||||
isApplicable: (structure: Structure) => structure.elementCount > 0,
|
||||
});
|
||||
65
src/extensions/mvs/components/annotation-label/visual.ts
Normal file
65
src/extensions/mvs/components/annotation-label/visual.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Text } from '../../../../mol-geo/geometry/text/text';
|
||||
import { TextBuilder } from '../../../../mol-geo/geometry/text/text-builder';
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { ComplexTextVisual, ComplexVisual } from '../../../../mol-repr/structure/complex-visual';
|
||||
import * as Original from '../../../../mol-repr/structure/visual/label-text';
|
||||
import { ElementIterator, eachSerialElement, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { VisualUpdateState } from '../../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../../mol-repr/visual';
|
||||
import { Theme } from '../../../../mol-theme/theme';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { omitObjectKeys } from '../../../../mol-util/object';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { textPropsForSelection } from '../../helpers/label-text';
|
||||
import { groupRows } from '../../helpers/selections';
|
||||
import { getMVSAnnotationForStructure } from '../annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for "label-text" visual in "MVS Annotation Label" representation */
|
||||
export type MVSAnnotationLabelTextParams = typeof MVSAnnotationLabelTextParams
|
||||
export const MVSAnnotationLabelTextParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property', isEssential: true }),
|
||||
fieldName: PD.Text('label', { description: 'Annotation field (column) from which to take label contents', isEssential: true }),
|
||||
...omitObjectKeys(Original.LabelTextParams, ['level', 'chainScale', 'residueScale', 'elementScale']),
|
||||
borderColor: { ...Original.LabelTextParams.borderColor, defaultValue: ColorNames.black },
|
||||
};
|
||||
|
||||
/** Parameter values for "label-text" visual in "MVS Annotation Label" representation */
|
||||
export type MVSAnnotationLabelTextProps = PD.Values<MVSAnnotationLabelTextParams>
|
||||
|
||||
/** Create "label-text" visual for "MVS Annotation Label" representation */
|
||||
export function MVSAnnotationLabelTextVisual(materialId: number): ComplexVisual<MVSAnnotationLabelTextParams> {
|
||||
return ComplexTextVisual<MVSAnnotationLabelTextParams>({
|
||||
defaultProps: PD.getDefaultValues(MVSAnnotationLabelTextParams),
|
||||
createGeometry: createLabelText,
|
||||
createLocationIterator: ElementIterator.fromStructure,
|
||||
getLoci: getSerialElementLoci,
|
||||
eachLocation: eachSerialElement,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<MVSAnnotationLabelTextParams>, currentProps: PD.Values<MVSAnnotationLabelTextParams>) => {
|
||||
state.createGeometry = newProps.annotationId !== currentProps.annotationId || newProps.fieldName !== currentProps.fieldName;
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: MVSAnnotationLabelTextProps, text?: Text): Text {
|
||||
const { annotation, model } = getMVSAnnotationForStructure(structure, props.annotationId);
|
||||
const rows = annotation?.getRows() ?? [];
|
||||
const { count, offsets, grouped } = groupRows(rows);
|
||||
const builder = TextBuilder.create(props, count, count / 2, text);
|
||||
for (let iGroup = 0; iGroup < count; iGroup++) {
|
||||
const iFirstRowInGroup = grouped[offsets[iGroup]];
|
||||
const labelText = annotation!.getValueForRow(iFirstRowInGroup, props.fieldName);
|
||||
if (!labelText) continue;
|
||||
const rowsInGroup = grouped.slice(offsets[iGroup], offsets[iGroup + 1]).map(j => rows[j]);
|
||||
const p = textPropsForSelection(structure, theme.size.size, rowsInGroup, model);
|
||||
if (!p) continue;
|
||||
builder.add(labelText, p.center[0], p.center[1], p.center[2], p.depth, p.scale, p.group);
|
||||
}
|
||||
return builder.getText();
|
||||
}
|
||||
383
src/extensions/mvs/components/annotation-prop.ts
Normal file
383
src/extensions/mvs/components/annotation-prop.ts
Normal file
@@ -0,0 +1,383 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../../mol-data/db';
|
||||
import { CIF, CifBlock, CifCategory, CifFile } from '../../../mol-io/reader/cif';
|
||||
import { toTable } from '../../../mol-io/reader/cif/schema';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure/structure';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayExtend } from '../../../mol-util/array';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { Jsonable, canonicalJsonString } from '../../../mol-util/json';
|
||||
import { pickObjectKeys, promiseAllObj } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { AtomRanges } from '../helpers/atom-ranges';
|
||||
import { IndicesAndSortings } from '../helpers/indexing';
|
||||
import { MaybeStringParamDefinition } from '../helpers/param-definition';
|
||||
import { MVSAnnotationRow, MVSAnnotationSchema, getCifAnnotationSchema } from '../helpers/schemas';
|
||||
import { atomQualifies, getAtomRangesForRow } from '../helpers/selections';
|
||||
import { Maybe, safePromise } from '../helpers/utils';
|
||||
|
||||
|
||||
/** Allowed values for the annotation format parameter */
|
||||
const MVSAnnotationFormat = new Choice({ json: 'json', cif: 'cif', bcif: 'bcif' }, 'json');
|
||||
type MVSAnnotationFormat = Choice.Values<typeof MVSAnnotationFormat>
|
||||
const MVSAnnotationFormatTypes = { json: 'string', cif: 'string', bcif: 'binary' } as const satisfies { [format in MVSAnnotationFormat]: 'string' | 'binary' };
|
||||
|
||||
/** Parameter definition for custom model property "MVS Annotations" */
|
||||
export type MVSAnnotationsParams = typeof MVSAnnotationsParams
|
||||
export const MVSAnnotationsParams = {
|
||||
annotations: PD.ObjectList(
|
||||
{
|
||||
source: PD.MappedStatic('source-cif', {
|
||||
'source-cif': PD.EmptyGroup(),
|
||||
'url': PD.Group({
|
||||
url: PD.Text(''),
|
||||
format: MVSAnnotationFormat.PDSelect(),
|
||||
}),
|
||||
}),
|
||||
schema: MVSAnnotationSchema.PDSelect(),
|
||||
cifBlock: PD.MappedStatic('index', {
|
||||
index: PD.Group({ index: PD.Numeric(0, { min: 0, step: 1 }, { description: '0-based index of the block' }) }),
|
||||
header: PD.Group({ header: PD.Text(undefined, { description: 'Block header' }) }),
|
||||
}, { description: 'Specify which CIF block contains annotation data (only relevant when format=cif or format=bcif)' }),
|
||||
cifCategory: MaybeStringParamDefinition(undefined, { description: 'Specify which CIF category contains annotation data (only relevant when format=cif or format=bcif)' }),
|
||||
id: PD.Text('', { description: 'Arbitrary identifier that can be referenced by MVSAnnotationColorTheme' }),
|
||||
},
|
||||
obj => obj.id
|
||||
),
|
||||
};
|
||||
|
||||
/** Parameter values for custom model property "MVS Annotations" */
|
||||
export type MVSAnnotationsProps = PD.Values<MVSAnnotationsParams>
|
||||
|
||||
/** Parameter values for a single annotation within custom model property "MVS Annotations" */
|
||||
export type MVSAnnotationSpec = MVSAnnotationsProps['annotations'][number]
|
||||
|
||||
/** Describes the source of an annotation file */
|
||||
type MVSAnnotationSource = { kind: 'url', url: string, format: MVSAnnotationFormat } | { kind: 'source-cif' }
|
||||
|
||||
/** Data file with one or more (in case of CIF) annotations */
|
||||
type MVSAnnotationFile = { format: 'json', data: Jsonable } | { format: 'cif', data: CifFile }
|
||||
|
||||
/** Data for a single annotation */
|
||||
type MVSAnnotationData = { format: 'json', data: Jsonable } | { format: 'cif', data: CifCategory }
|
||||
|
||||
|
||||
/** Provider for custom model property "Annotations" */
|
||||
export const MVSAnnotationsProvider: CustomModelProperty.Provider<MVSAnnotationsParams, MVSAnnotations> = CustomModelProperty.createProvider({
|
||||
label: 'Annotations',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'mvs-annotations',
|
||||
}),
|
||||
type: 'static',
|
||||
defaultParams: MVSAnnotationsParams,
|
||||
getParams: (data: Model) => MVSAnnotationsParams,
|
||||
isApplicable: (data: Model) => true,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<MVSAnnotationsProps>) => {
|
||||
props = { ...PD.getDefaultValues(MVSAnnotationsParams), ...props };
|
||||
const specs: MVSAnnotationSpec[] = props.annotations ?? [];
|
||||
const annots = await MVSAnnotations.fromSpecs(ctx, specs, data);
|
||||
return { value: annots } satisfies CustomProperty.Data<MVSAnnotations>;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** Represents multiple annotations retrievable by their ID */
|
||||
export class MVSAnnotations {
|
||||
private constructor(private dict: { [id: string]: MVSAnnotation }) { }
|
||||
static async fromSpecs(ctx: CustomProperty.Context, specs: MVSAnnotationSpec[], model?: Model): Promise<MVSAnnotations> {
|
||||
const sources: MVSAnnotationSource[] = specs.map(annotationSourceFromSpec);
|
||||
const files = await getFilesFromSources(ctx, sources, model);
|
||||
const annots: { [id: string]: MVSAnnotation } = {};
|
||||
for (let i = 0; i < specs.length; i++) {
|
||||
const spec = specs[i];
|
||||
try {
|
||||
const file = files[i];
|
||||
if (!file.ok) throw file.error;
|
||||
annots[spec.id] = await MVSAnnotation.fromSpec(ctx, spec, file.value);
|
||||
} catch (err) {
|
||||
console.error(`Failed to obtain annotation (${err}).\nAnnotation specification:`, spec);
|
||||
annots[spec.id] = MVSAnnotation.createEmpty(spec.schema);
|
||||
}
|
||||
}
|
||||
return new MVSAnnotations(annots);
|
||||
}
|
||||
getAnnotation(id: string): MVSAnnotation | undefined {
|
||||
return this.dict[id];
|
||||
}
|
||||
getAllAnnotations(): MVSAnnotation[] {
|
||||
return Object.values(this.dict);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Retrieve annotation with given `annotationId` from custom model property "MVS Annotations" and the model from which it comes */
|
||||
export function getMVSAnnotationForStructure(structure: Structure, annotationId: string): { annotation: MVSAnnotation, model: Model } | { annotation: undefined, model: undefined } {
|
||||
const models = structure.isEmpty ? [] : structure.models;
|
||||
for (const model of models) {
|
||||
if (model.customProperties.has(MVSAnnotationsProvider.descriptor)) {
|
||||
const annots = MVSAnnotationsProvider.get(model).value;
|
||||
const annotation = annots?.getAnnotation(annotationId);
|
||||
if (annotation) {
|
||||
return { annotation, model };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { annotation: undefined, model: undefined };
|
||||
}
|
||||
|
||||
/** Main class for processing MVS annotation */
|
||||
export class MVSAnnotation {
|
||||
/** Store mapping `ElementIndex` -> annotation row index for each `Model`, -1 means no row applies */
|
||||
private indexedModels = new Map<UUID, number[]>();
|
||||
private rows: MVSAnnotationRow[] | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
public data: MVSAnnotationData,
|
||||
public schema: MVSAnnotationSchema,
|
||||
) { }
|
||||
|
||||
/** Create a new `MVSAnnotation` based on specification `spec`. Use `file` if provided, otherwise download the file.
|
||||
* Throw error if download fails or problem with data. */
|
||||
static async fromSpec(ctx: CustomProperty.Context, spec: MVSAnnotationSpec, file?: MVSAnnotationFile): Promise<MVSAnnotation> {
|
||||
file ??= await getFileFromSource(ctx, annotationSourceFromSpec(spec));
|
||||
|
||||
let data: MVSAnnotationData;
|
||||
switch (file.format) {
|
||||
case 'json':
|
||||
data = file;
|
||||
break;
|
||||
case 'cif':
|
||||
if (file.data.blocks.length === 0) throw new Error('No block in CIF');
|
||||
const blockSpec = spec.cifBlock;
|
||||
let block: CifBlock;
|
||||
switch (blockSpec.name) {
|
||||
case 'header':
|
||||
const foundBlock = file.data.blocks.find(b => b.header === blockSpec.params.header);
|
||||
if (!foundBlock) throw new Error(`CIF block with header ${blockSpec.params.header} not found`);
|
||||
block = foundBlock;
|
||||
break;
|
||||
case 'index':
|
||||
block = file.data.blocks[blockSpec.params.index];
|
||||
if (!block) throw new Error(`CIF block with index ${blockSpec.params.index} not found`);
|
||||
break;
|
||||
}
|
||||
const categoryName = spec.cifCategory ?? Object.keys(block.categories)[0];
|
||||
if (!categoryName) throw new Error('There are no categories in CIF block');
|
||||
const category = block.categories[categoryName];
|
||||
if (!category) throw new Error(`CIF category ${categoryName} not found`);
|
||||
data = { format: 'cif', data: category };
|
||||
break;
|
||||
}
|
||||
return new MVSAnnotation(data, spec.schema);
|
||||
}
|
||||
|
||||
static createEmpty(schema: MVSAnnotationSchema): MVSAnnotation {
|
||||
return new MVSAnnotation({ format: 'json', data: [] }, schema);
|
||||
}
|
||||
|
||||
/** Reference implementation of `getAnnotationForLocation`, just for checking, DO NOT USE DIRECTLY */
|
||||
getAnnotationForLocation_Reference(loc: StructureElement.Location): MVSAnnotationRow | undefined {
|
||||
const model = loc.unit.model;
|
||||
const iAtom = loc.element;
|
||||
let result: MVSAnnotationRow | undefined = undefined;
|
||||
for (const row of this.getRows()) {
|
||||
if (atomQualifies(model, iAtom, row)) result = row;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return value of field `fieldName` assigned to location `loc`, if any */
|
||||
getValueForLocation(loc: StructureElement.Location, fieldName: string): string | undefined {
|
||||
const indexedModel = this.getIndexedModel(loc.unit.model);
|
||||
const iRow = indexedModel[loc.element];
|
||||
return this.getValueForRow(iRow, fieldName);
|
||||
}
|
||||
/** Return value of field `fieldName` assigned to `i`-th annotation row, if any */
|
||||
getValueForRow(i: number, fieldName: string): string | undefined {
|
||||
if (i < 0) return undefined;
|
||||
switch (this.data.format) {
|
||||
case 'json':
|
||||
const value = getValueFromJson(i, fieldName, this.data.data);
|
||||
if (value === undefined || typeof value === 'string') return value;
|
||||
else return `${value}`;
|
||||
case 'cif':
|
||||
return getValueFromCif(i, fieldName, this.data.data);
|
||||
}
|
||||
}
|
||||
|
||||
/** Return cached `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` (or create it if not cached yet) */
|
||||
private getIndexedModel(model: Model): number[] {
|
||||
const key = model.id;
|
||||
if (!this.indexedModels.has(key)) {
|
||||
const result = this.getRowForEachAtom(model);
|
||||
this.indexedModels.set(key, result);
|
||||
}
|
||||
return this.indexedModels.get(key)!;
|
||||
}
|
||||
|
||||
/** Create `ElementIndex` -> `MVSAnnotationRow` mapping for `Model` */
|
||||
private getRowForEachAtom(model: Model): number[] {
|
||||
const indices = IndicesAndSortings.get(model);
|
||||
const nAtoms = model.atomicHierarchy.atoms._rowCount;
|
||||
const result: number[] = Array(nAtoms).fill(-1);
|
||||
const rows = this.getRows();
|
||||
for (let i = 0, nRows = rows.length; i < nRows; i++) {
|
||||
const atomRanges = getAtomRangesForRow(model, rows[i], indices);
|
||||
AtomRanges.foreach(atomRanges, (from, to) => result.fill(i, from, to));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Parse and return all annotation rows in this annotation */
|
||||
private _getRows(): MVSAnnotationRow[] {
|
||||
switch (this.data.format) {
|
||||
case 'json':
|
||||
return getRowsFromJson(this.data.data, this.schema);
|
||||
case 'cif':
|
||||
return getRowsFromCif(this.data.data, this.schema);
|
||||
}
|
||||
}
|
||||
/** Parse and return all annotation rows in this annotation, or return cached result if available */
|
||||
getRows(): readonly MVSAnnotationRow[] {
|
||||
return this.rows ??= this._getRows();
|
||||
}
|
||||
}
|
||||
|
||||
function getValueFromJson<T>(rowIndex: number, fieldName: string, data: Jsonable): T | undefined {
|
||||
const js = data as any;
|
||||
if (Array.isArray(js)) {
|
||||
const row = js[rowIndex] ?? {};
|
||||
return row[fieldName];
|
||||
} else {
|
||||
const column = js[fieldName] ?? [];
|
||||
return column[rowIndex];
|
||||
}
|
||||
}
|
||||
function getValueFromCif(rowIndex: number, fieldName: string, data: CifCategory): string | undefined {
|
||||
const column = data.getField(fieldName);
|
||||
if (!column) return undefined;
|
||||
if (column.valueKind(rowIndex) !== Column.ValueKind.Present) return undefined;
|
||||
return column.str(rowIndex);
|
||||
}
|
||||
|
||||
function getRowsFromJson(data: Jsonable, schema: MVSAnnotationSchema): MVSAnnotationRow[] {
|
||||
const js = data as any;
|
||||
const cifSchema = getCifAnnotationSchema(schema);
|
||||
if (Array.isArray(js)) {
|
||||
// array of objects
|
||||
return js.map(row => pickObjectKeys(row, Object.keys(cifSchema)));
|
||||
} else {
|
||||
// object of arrays
|
||||
const rows: MVSAnnotationRow[] = [];
|
||||
const keys = Object.keys(js).filter(key => Object.hasOwn(cifSchema, key as any));
|
||||
if (keys.length > 0) {
|
||||
const n = js[keys[0]].length;
|
||||
if (keys.some(key => js[key].length !== n)) throw new Error('FormatError: arrays must have the same length.');
|
||||
for (let i = 0; i < n; i++) {
|
||||
const item: { [key: string]: any } = {};
|
||||
for (const key of keys) {
|
||||
item[key] = js[key][i];
|
||||
}
|
||||
rows.push(item);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
}
|
||||
|
||||
function getRowsFromCif(data: CifCategory, schema: MVSAnnotationSchema): MVSAnnotationRow[] {
|
||||
const rows: MVSAnnotationRow[] = [];
|
||||
const cifSchema = getCifAnnotationSchema(schema);
|
||||
const table = toTable(cifSchema, data);
|
||||
arrayExtend(rows, getRowsFromTable(table)); // Avoiding Table.getRows(table) as it replaces . and ? fields by 0 or ''
|
||||
return rows;
|
||||
}
|
||||
|
||||
/** Same as `Table.getRows` but omits `.` and `?` fields (instead of using type defaults) */
|
||||
function getRowsFromTable<S extends Table.Schema>(table: Table<S>): Partial<Table.Row<S>>[] {
|
||||
const rows: Partial<Table.Row<S>>[] = [];
|
||||
const columns = table._columns;
|
||||
const nRows = table._rowCount;
|
||||
const Present = Column.ValueKind.Present;
|
||||
for (let iRow = 0; iRow < nRows; iRow++) {
|
||||
const row: Partial<Table.Row<S>> = {};
|
||||
for (const col of columns) {
|
||||
if (table[col].valueKind(iRow) === Present) {
|
||||
row[col as keyof S] = table[col].value(iRow);
|
||||
}
|
||||
}
|
||||
rows[iRow] = row;
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getFileFromSource(ctx: CustomProperty.Context, source: MVSAnnotationSource, model?: Model): Promise<MVSAnnotationFile> {
|
||||
switch (source.kind) {
|
||||
case 'source-cif':
|
||||
return { format: 'cif', data: getSourceFileFromModel(model) };
|
||||
case 'url':
|
||||
const url = Asset.getUrlAsset(ctx.assetManager, source.url);
|
||||
const dataType = MVSAnnotationFormatTypes[source.format];
|
||||
const dataWrapper = await ctx.assetManager.resolve(url, dataType).runInContext(ctx.runtime);
|
||||
const rawData = dataWrapper.data;
|
||||
if (!rawData) throw new Error('Missing data');
|
||||
switch (source.format) {
|
||||
case 'json':
|
||||
const json = JSON.parse(rawData as string) as Jsonable;
|
||||
return { format: 'json', data: json };
|
||||
case 'cif':
|
||||
case 'bcif':
|
||||
const parsed = await CIF.parse(rawData).run();
|
||||
if (parsed.isError) throw new Error(`Failed to parse ${source.format}`);
|
||||
return { format: 'cif', data: parsed.result };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Like `sources.map(s => safePromise(getFileFromSource(ctx, s)))`
|
||||
* but downloads a repeating source only once. */
|
||||
async function getFilesFromSources(ctx: CustomProperty.Context, sources: MVSAnnotationSource[], model?: Model): Promise<Maybe<MVSAnnotationFile>[]> {
|
||||
const promises: { [key: string]: Promise<Maybe<MVSAnnotationFile>> } = {};
|
||||
for (const src of sources) {
|
||||
const key = canonicalJsonString(src);
|
||||
promises[key] ??= safePromise(getFileFromSource(ctx, src, model));
|
||||
}
|
||||
const files = await promiseAllObj(promises);
|
||||
return sources.map(src => files[canonicalJsonString(src)]);
|
||||
}
|
||||
|
||||
function getSourceFileFromModel(model?: Model): CifFile {
|
||||
if (model && MmcifFormat.is(model.sourceData)) {
|
||||
if (model.sourceData.data.file) {
|
||||
return model.sourceData.data.file;
|
||||
} else {
|
||||
const frame = model.sourceData.data.frame;
|
||||
const block = CifBlock(Array.from(frame.categoryNames), frame.categories, frame.header);
|
||||
const file = CifFile([block]);
|
||||
return file;
|
||||
}
|
||||
} else {
|
||||
console.warn('Could not get CifFile from Model, returning empty CifFile');
|
||||
return CifFile([]);
|
||||
}
|
||||
}
|
||||
|
||||
function annotationSourceFromSpec(s: MVSAnnotationSpec): MVSAnnotationSource {
|
||||
switch (s.source.name) {
|
||||
case 'url':
|
||||
return { kind: 'url', ...s.source.params };
|
||||
case 'source-cif':
|
||||
return { kind: 'source-cif' };
|
||||
}
|
||||
}
|
||||
116
src/extensions/mvs/components/annotation-structure-component.ts
Normal file
116
src/extensions/mvs/components/annotation-structure-component.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure, StructureSelection } from '../../../mol-model/structure';
|
||||
import { StructureQueryHelper } from '../../../mol-plugin-state/helpers/structure-query';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
import { StateObject, StateTransformer } from '../../../mol-state';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { omitObjectKeys } from '../../../mol-util/object';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { rowsToExpression } from '../helpers/selections';
|
||||
import { getMVSAnnotationForStructure } from './annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for `MVSAnnotationStructureComponent` transformer */
|
||||
export const MVSAnnotationStructureComponentParams = {
|
||||
annotationId: PD.Text('', { description: 'Reference to "Annotation" custom model property' }),
|
||||
fieldName: PD.Text('component', { description: 'Annotation field (column) from which to take component identifier' }),
|
||||
fieldValues: PD.MappedStatic('all', {
|
||||
all: PD.EmptyGroup(),
|
||||
selected: PD.ObjectList({
|
||||
value: PD.Text(),
|
||||
}, obj => obj.value),
|
||||
}),
|
||||
nullIfEmpty: PD.Optional(PD.Boolean(true, { isHidden: false })),
|
||||
label: PD.Text('', { isHidden: false }),
|
||||
};
|
||||
|
||||
/** Parameter values for `MVSAnnotationStructureComponent` transformer */
|
||||
export type MVSAnnotationStructureComponentProps = PD.ValuesFor<typeof MVSAnnotationStructureComponentParams>
|
||||
|
||||
|
||||
/** Transformer builder for MVS extension */
|
||||
export const MVSTransform = StateTransformer.builderFactory('mvs');
|
||||
|
||||
/** Transformer for creating a structure component based on custom model property "Annotations" */
|
||||
export type MVSAnnotationStructureComponent = typeof MVSAnnotationStructureComponent
|
||||
export const MVSAnnotationStructureComponent = MVSTransform({
|
||||
name: 'mvs-structure-component-from-annotation',
|
||||
display: { name: 'MVS Annotation Component', description: 'A molecular structure component defined by MVS annotation data.' },
|
||||
from: SO.Molecule.Structure,
|
||||
to: SO.Molecule.Structure,
|
||||
params: MVSAnnotationStructureComponentParams,
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return createMVSAnnotationStructureComponent(a.data, params);
|
||||
},
|
||||
update: ({ a, b, oldParams, newParams }) => {
|
||||
return updateMVSAnnotationStructureComponent(a.data, b, oldParams, newParams);
|
||||
},
|
||||
dispose({ b }) {
|
||||
b?.data.customPropertyDescriptors.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** Create a substructure based on `MVSAnnotationStructureComponentProps` */
|
||||
export function createMVSAnnotationSubstructure(structure: Structure, params: MVSAnnotationStructureComponentProps): Structure {
|
||||
const { annotation } = getMVSAnnotationForStructure(structure, params.annotationId);
|
||||
if (annotation) {
|
||||
let rows = annotation.getRows();
|
||||
if (params.fieldValues.name === 'selected') {
|
||||
const selectedValues = new Set<string | undefined>(params.fieldValues.params.map(obj => obj.value));
|
||||
rows = rows.filter((row, i) => selectedValues.has(annotation.getValueForRow(i, params.fieldName)));
|
||||
}
|
||||
const expression = rowsToExpression(rows);
|
||||
|
||||
const { selection } = StructureQueryHelper.createAndRun(structure, expression);
|
||||
return StructureSelection.unionStructure(selection);
|
||||
} else {
|
||||
return Structure.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function createMVSAnnotationStructureComponent(structure: Structure, params: MVSAnnotationStructureComponentProps) {
|
||||
const component = createMVSAnnotationSubstructure(structure, params);
|
||||
|
||||
if (params.nullIfEmpty && component.elementCount === 0) return StateObject.Null;
|
||||
|
||||
let label = params.label;
|
||||
if (label === undefined || label === '') {
|
||||
if (params.fieldValues.name === 'selected' && params.fieldValues.params.length > 0) {
|
||||
const values = params.fieldValues.params;
|
||||
let valuesStr = `"${values[0].value}"`;
|
||||
if (values.length === 2) {
|
||||
valuesStr += ` + "${values[1].value}"`;
|
||||
} else if (values.length > 2) {
|
||||
valuesStr += ` + ${values.length - 1} more values`;
|
||||
}
|
||||
label = `MVS Annotation Component (${params.fieldName}: ${valuesStr})`;
|
||||
} else {
|
||||
label = 'MVS Annotation Component';
|
||||
}
|
||||
}
|
||||
|
||||
const props = { label, description: Structure.elementDescription(component) };
|
||||
return new SO.Molecule.Structure(component, props);
|
||||
}
|
||||
|
||||
/** Update a substructure PSO based on `MVSAnnotationStructureComponentProps` */
|
||||
export function updateMVSAnnotationStructureComponent(a: Structure, b: SO.Molecule.Structure, oldParams: MVSAnnotationStructureComponentProps, newParams: MVSAnnotationStructureComponentProps) {
|
||||
const change = !deepEqual(newParams, oldParams);
|
||||
const needsRecreate = !deepEqual(omitObjectKeys(newParams, ['label']), omitObjectKeys(oldParams, ['label']));
|
||||
if (!change) {
|
||||
return StateTransformer.UpdateResult.Unchanged;
|
||||
}
|
||||
if (!needsRecreate) {
|
||||
b.label = newParams.label || b.label;
|
||||
return StateTransformer.UpdateResult.Updated;
|
||||
}
|
||||
return StateTransformer.UpdateResult.Recreate;
|
||||
}
|
||||
68
src/extensions/mvs/components/annotation-tooltips-prop.ts
Normal file
68
src/extensions/mvs/components/annotation-tooltips-prop.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomStructureProperty } from '../../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { filterDefined } from '../helpers/utils';
|
||||
import { MVSAnnotationsProvider } from './annotation-prop';
|
||||
|
||||
|
||||
/** Parameter definition for custom structure property "MVSAnnotationTooltips" */
|
||||
export const MVSAnnotationTooltipsParams = {
|
||||
tooltips: PD.ObjectList(
|
||||
{
|
||||
annotationId: PD.Text('', { description: 'Reference to "MVS Annotation" custom model property' }),
|
||||
fieldName: PD.Text('tooltip', { description: 'Annotation field (column) from which to take color values' }),
|
||||
},
|
||||
obj => `${obj.annotationId}:${obj.fieldName}`
|
||||
),
|
||||
};
|
||||
export type MVSAnnotationTooltipsParams = typeof MVSAnnotationTooltipsParams
|
||||
|
||||
/** Values of custom structure property "MVSAnnotationTooltips" (and for its params at the same type) */
|
||||
export type MVSAnnotationTooltipsProps = PD.Values<MVSAnnotationTooltipsParams>
|
||||
|
||||
|
||||
/** Provider for custom structure property "MVSAnnotationTooltips" */
|
||||
export const MVSAnnotationTooltipsProvider: CustomStructureProperty.Provider<MVSAnnotationTooltipsParams, MVSAnnotationTooltipsProps> = CustomStructureProperty.createProvider({
|
||||
label: 'MVS Annotation Tooltips',
|
||||
descriptor: CustomPropertyDescriptor<any, any>({
|
||||
name: 'mvs-annotation-tooltips',
|
||||
}),
|
||||
type: 'local',
|
||||
defaultParams: MVSAnnotationTooltipsParams,
|
||||
getParams: (data: Structure) => MVSAnnotationTooltipsParams,
|
||||
isApplicable: (data: Structure) => data.root === data,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<MVSAnnotationTooltipsProps>) => {
|
||||
const fullProps = { ...PD.getDefaultValues(MVSAnnotationTooltipsParams), ...props };
|
||||
return { value: fullProps } satisfies CustomProperty.Data<MVSAnnotationTooltipsProps>;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/** Label provider based on data from "MVS Annotation" custom model property */
|
||||
export const MVSAnnotationTooltipsLabelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
switch (loci.kind) {
|
||||
case 'element-loci':
|
||||
if (!loci.structure.customPropertyDescriptors.hasReference(MVSAnnotationTooltipsProvider.descriptor)) return undefined;
|
||||
const location = StructureElement.Loci.getFirstLocation(loci);
|
||||
if (!location) return undefined;
|
||||
const tooltipProps = MVSAnnotationTooltipsProvider.get(location.structure).value;
|
||||
if (!tooltipProps || tooltipProps.tooltips.length === 0) return undefined;
|
||||
const annotations = MVSAnnotationsProvider.get(location.unit.model).value;
|
||||
const texts = tooltipProps.tooltips.map(p => annotations?.getAnnotation(p.annotationId)?.getValueForLocation(location, p.fieldName));
|
||||
return filterDefined(texts).join(' | ');
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} satisfies LociLabelProvider;
|
||||
49
src/extensions/mvs/components/custom-label/representation.ts
Normal file
49
src/extensions/mvs/components/custom-label/representation.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../../mol-repr/representation';
|
||||
import { ComplexRepresentation, StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder } from '../../../../mol-repr/structure/representation';
|
||||
import { MarkerAction } from '../../../../mol-util/marker-action';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { CustomLabelTextParams, CustomLabelTextVisual } from './visual';
|
||||
|
||||
|
||||
/** Components of "Custom Label" representation */
|
||||
const CustomLabelVisuals = {
|
||||
'label-text': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CustomLabelTextParams>) => ComplexRepresentation('Label text', ctx, getParams, CustomLabelTextVisual),
|
||||
};
|
||||
|
||||
/** Parameter definition for representation type "Custom Label" */
|
||||
export type CustomLabelParams = typeof CustomLabelParams
|
||||
export const CustomLabelParams = {
|
||||
...CustomLabelTextParams,
|
||||
visuals: PD.MultiSelect(['label-text'], PD.objectToOptions(CustomLabelVisuals)),
|
||||
};
|
||||
|
||||
/** Parameter values for representation type "Custom Label" */
|
||||
export type CustomLabelProps = PD.ValuesFor<CustomLabelParams>
|
||||
|
||||
/** Structure representation type "Custom Label", allowing user-defined labels at at user-defined positions */
|
||||
export type CustomLabelRepresentation = StructureRepresentation<CustomLabelParams>
|
||||
export function CustomLabelRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, CustomLabelParams>): CustomLabelRepresentation {
|
||||
const repr = Representation.createMulti('Label', ctx, getParams, StructureRepresentationStateBuilder, CustomLabelVisuals as unknown as Representation.Def<Structure, CustomLabelParams>);
|
||||
repr.setState({ pickable: false, markerActions: MarkerAction.None });
|
||||
return repr;
|
||||
}
|
||||
|
||||
/** A thingy that is needed to register representation type "Custom Label", allowing user-defined labels at at user-defined positions */
|
||||
export const CustomLabelRepresentationProvider = StructureRepresentationProvider({
|
||||
name: 'mvs-custom-label',
|
||||
label: 'Custom Label',
|
||||
description: 'Displays labels with custom text',
|
||||
factory: CustomLabelRepresentation,
|
||||
getParams: () => CustomLabelParams,
|
||||
defaultValues: PD.getDefaultValues(CustomLabelParams),
|
||||
defaultColorTheme: { name: 'uniform' },
|
||||
defaultSizeTheme: { name: 'physical' },
|
||||
isApplicable: (structure: Structure) => structure.elementCount > 0
|
||||
});
|
||||
98
src/extensions/mvs/components/custom-label/visual.ts
Normal file
98
src/extensions/mvs/components/custom-label/visual.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../../mol-data/int';
|
||||
import { Text } from '../../../../mol-geo/geometry/text/text';
|
||||
import { TextBuilder } from '../../../../mol-geo/geometry/text/text-builder';
|
||||
import { Structure } from '../../../../mol-model/structure';
|
||||
import { ComplexTextVisual, ComplexVisual } from '../../../../mol-repr/structure/complex-visual';
|
||||
import * as Original from '../../../../mol-repr/structure/visual/label-text';
|
||||
import { ElementIterator, eachSerialElement, getSerialElementLoci } from '../../../../mol-repr/structure/visual/util/element';
|
||||
import { VisualUpdateState } from '../../../../mol-repr/util';
|
||||
import { VisualContext } from '../../../../mol-repr/visual';
|
||||
import { Theme } from '../../../../mol-theme/theme';
|
||||
import { deepEqual } from '../../../../mol-util';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { omitObjectKeys } from '../../../../mol-util/object';
|
||||
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
|
||||
import { textPropsForSelection } from '../../helpers/label-text';
|
||||
import { SelectorParams, substructureFromSelector } from '../selector';
|
||||
|
||||
|
||||
/** Parameter definition for "label-text" visual in "Custom Label" representation */
|
||||
export type CustomLabelTextParams = typeof CustomLabelTextParams
|
||||
export const CustomLabelTextParams = {
|
||||
items: PD.ObjectList(
|
||||
{
|
||||
text: PD.Text('¯\\_(ツ)_/¯'),
|
||||
position: PD.MappedStatic('selection', {
|
||||
x_y_z: PD.Group({
|
||||
x: PD.Numeric(0),
|
||||
y: PD.Numeric(0),
|
||||
z: PD.Numeric(0),
|
||||
scale: PD.Numeric(1, { min: 0, max: 20, step: 0.1 })
|
||||
}),
|
||||
selection: PD.Group({
|
||||
selector: SelectorParams,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
obj => obj.text,
|
||||
{ isEssential: true }
|
||||
),
|
||||
...omitObjectKeys(Original.LabelTextParams, ['level', 'chainScale', 'residueScale', 'elementScale']),
|
||||
borderColor: { ...Original.LabelTextParams.borderColor, defaultValue: ColorNames.black },
|
||||
};
|
||||
|
||||
/** Parameter values for "label-text" visual in "Custom Label" representation */
|
||||
export type CustomLabelTextProps = PD.Values<CustomLabelTextParams>
|
||||
|
||||
/** Create "label-text" visual for "Custom Label" representation */
|
||||
export function CustomLabelTextVisual(materialId: number): ComplexVisual<CustomLabelTextParams> {
|
||||
return ComplexTextVisual<CustomLabelTextParams>({
|
||||
defaultProps: PD.getDefaultValues(CustomLabelTextParams),
|
||||
createGeometry: createLabelText,
|
||||
createLocationIterator: ElementIterator.fromStructure,
|
||||
getLoci: getSerialElementLoci,
|
||||
eachLocation: eachSerialElement,
|
||||
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<CustomLabelTextParams>, currentProps: PD.Values<CustomLabelTextParams>) => {
|
||||
state.createGeometry = !deepEqual(newProps.items, currentProps.items);
|
||||
}
|
||||
}, materialId);
|
||||
}
|
||||
|
||||
function createLabelText(ctx: VisualContext, structure: Structure, theme: Theme, props: CustomLabelTextProps, text?: Text): Text {
|
||||
const count = props.items.length;
|
||||
const builder = TextBuilder.create(props, count, count / 2, text);
|
||||
for (const item of props.items) {
|
||||
switch (item.position.name) {
|
||||
case 'x_y_z':
|
||||
const scale = item.position.params.scale;
|
||||
builder.add(item.text, item.position.params.x, item.position.params.y, item.position.params.z, scale, scale, 0);
|
||||
break;
|
||||
case 'selection':
|
||||
const substructure = substructureFromSelector(structure, item.position.params.selector);
|
||||
const p = textPropsForSelection(substructure, theme.size.size, {});
|
||||
const group = serialIndexOfSubstructure(structure, substructure) ?? 0;
|
||||
if (p) builder.add(item.text, p.center[0], p.center[1], p.center[2], p.depth, p.scale, group);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return builder.getText();
|
||||
}
|
||||
|
||||
/** Return the serial index within `structure` of the first element of `substructure` (or `undefined` in that element is not in `structure`) */
|
||||
function serialIndexOfSubstructure(structure: Structure, substructure: Structure): number | undefined {
|
||||
if (substructure.isEmpty) return undefined;
|
||||
const theUnit = substructure.units[0];
|
||||
const theElement = theUnit.elements[0];
|
||||
for (const unit of structure.units) {
|
||||
if (unit.model.id === theUnit.model.id && SortedArray.has(unit.elements, theElement)) {
|
||||
return structure.serialMapping.getSerialIndex(unit, theElement);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
78
src/extensions/mvs/components/custom-tooltips-prop.ts
Normal file
78
src/extensions/mvs/components/custom-tooltips-prop.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { CustomStructureProperty } from '../../../mol-model-props/common/custom-structure-property';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { filterDefined } from '../helpers/utils';
|
||||
import { ElementSet, Selector, SelectorParams } from './selector';
|
||||
|
||||
|
||||
/** Parameter definition for custom structure property "CustomTooltips" */
|
||||
export type CustomTooltipsParams = typeof CustomTooltipsParams
|
||||
export const CustomTooltipsParams = {
|
||||
tooltips: PD.ObjectList(
|
||||
{
|
||||
text: PD.Text('', { description: 'Text of the tooltip' }),
|
||||
selector: SelectorParams,
|
||||
},
|
||||
obj => obj.text
|
||||
),
|
||||
};
|
||||
|
||||
/** Parameter values of custom structure property "CustomTooltips" */
|
||||
export type CustomTooltipsProps = PD.Values<CustomTooltipsParams>
|
||||
|
||||
/** Values of custom structure property "CustomTooltips" (and for its params at the same type) */
|
||||
export type CustomTooltipsData = { selector: Selector, text: string, elementSet?: ElementSet }[]
|
||||
|
||||
|
||||
/** Provider for custom structure property "CustomTooltips" */
|
||||
export const CustomTooltipsProvider: CustomStructureProperty.Provider<CustomTooltipsParams, CustomTooltipsData> = CustomStructureProperty.createProvider({
|
||||
label: 'Custom Tooltips',
|
||||
descriptor: CustomPropertyDescriptor<any, any>({
|
||||
name: 'mvs-custom-tooltips',
|
||||
}),
|
||||
type: 'local',
|
||||
defaultParams: CustomTooltipsParams,
|
||||
getParams: (data: Structure) => CustomTooltipsParams,
|
||||
isApplicable: (data: Structure) => data.root === data,
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<CustomTooltipsProps>) => {
|
||||
const fullProps = { ...PD.getDefaultValues(CustomTooltipsParams), ...props };
|
||||
const value = fullProps.tooltips.map(t => ({
|
||||
selector: t.selector,
|
||||
text: t.text,
|
||||
} satisfies CustomTooltipsData[number]));
|
||||
return { value: value } satisfies CustomProperty.Data<CustomTooltipsData>;
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
/** Label provider based on custom structure property "CustomTooltips" */
|
||||
export const CustomTooltipsLabelProvider = {
|
||||
label: (loci: Loci): string | undefined => {
|
||||
switch (loci.kind) {
|
||||
case 'element-loci':
|
||||
if (!loci.structure.customPropertyDescriptors.hasReference(CustomTooltipsProvider.descriptor)) return undefined;
|
||||
const location = StructureElement.Loci.getFirstLocation(loci);
|
||||
if (!location) return undefined;
|
||||
const tooltipData = CustomTooltipsProvider.get(location.structure).value;
|
||||
if (!tooltipData || tooltipData.length === 0) return undefined;
|
||||
const texts = [];
|
||||
for (const tooltip of tooltipData) {
|
||||
const elements = tooltip.elementSet ??= ElementSet.fromSelector(location.structure, tooltip.selector);
|
||||
if (ElementSet.has(elements, location)) texts.push(tooltip.text);
|
||||
}
|
||||
return filterDefined(texts).join(' | ');
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} satisfies LociLabelProvider;
|
||||
77
src/extensions/mvs/components/formats.ts
Normal file
77
src/extensions/mvs/components/formats.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { DataFormatProvider } from '../../../mol-plugin-state/formats/provider';
|
||||
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
|
||||
import { Download } from '../../../mol-plugin-state/transforms/data';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateAction, StateObjectRef } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { loadMVS } from '../load';
|
||||
import { MVSData } from '../mvs-data';
|
||||
import { MVSTransform } from './annotation-structure-component';
|
||||
|
||||
|
||||
/** Plugin state object storing `MVSData` */
|
||||
export class Mvs extends SO.Create<{ mvsData: MVSData, sourceUrl?: string }>({ name: 'MVS Data', typeClass: 'Data' }) { }
|
||||
|
||||
/** Transformer for parsing data in MVSJ format */
|
||||
export const ParseMVSJ = MVSTransform({
|
||||
name: 'mvs-parse-mvsj',
|
||||
display: { name: 'MVS Annotation Component', description: 'A molecular structure component defined by MVS annotation data.' },
|
||||
from: SO.Data.String,
|
||||
to: Mvs,
|
||||
})({
|
||||
apply({ a }, plugin: PluginContext) {
|
||||
const mvsData = MVSData.fromMVSJ(a.data);
|
||||
const sourceUrl = tryGetDownloadUrl(a, plugin);
|
||||
return new Mvs({ mvsData, sourceUrl });
|
||||
},
|
||||
});
|
||||
|
||||
/** If the PluginStateObject `pso` comes from a Download transform, try to get its `url` parameter. */
|
||||
function tryGetDownloadUrl(pso: SO.Data.String, plugin: PluginContext): string | undefined {
|
||||
const theCell = plugin.state.data.selectQ(q => q.ofTransformer(Download)).find(cell => cell.obj === pso);
|
||||
const urlParam = theCell?.transform.params?.url;
|
||||
return urlParam ? Asset.getUrl(urlParam) : undefined;
|
||||
}
|
||||
|
||||
|
||||
/** Params for the `LoadMvsData` action */
|
||||
const LoadMvsDataParams = {
|
||||
replaceExisting: PD.Boolean(false, { description: 'If true, the loaded MVS view will replace the current state; if false, the MVS view will be added to the current state.' }),
|
||||
};
|
||||
|
||||
/** State action which loads a MVS view into Mol* */
|
||||
export const LoadMvsData = StateAction.build({
|
||||
display: { name: 'Load MVS Data' },
|
||||
from: Mvs,
|
||||
params: LoadMvsDataParams,
|
||||
})(({ a, params }, plugin: PluginContext) => Task.create('Load MVS Data', async () => {
|
||||
const { mvsData, sourceUrl } = a.data;
|
||||
await loadMVS(plugin, mvsData, { replaceExisting: params.replaceExisting, sourceUrl: sourceUrl });
|
||||
}));
|
||||
|
||||
|
||||
/** Data format provider for MVSJ format.
|
||||
* If Visuals:On, it will load the parsed MVS view;
|
||||
* otherwise it will just create a plugin state object with parsed data. */
|
||||
export const MVSJFormatProvider: DataFormatProvider<{}, StateObjectRef<Mvs>, any> = DataFormatProvider({
|
||||
label: 'MVSJ',
|
||||
description: 'MVSJ',
|
||||
category: 'Miscellaneous',
|
||||
stringExtensions: ['mvsj'],
|
||||
parse: async (plugin, data) => {
|
||||
return plugin.state.data.build().to(data).apply(ParseMVSJ).commit();
|
||||
},
|
||||
visuals: async (plugin, data) => {
|
||||
const ref = StateObjectRef.resolveRef(data);
|
||||
const params = PD.getDefaultValues(LoadMvsDataParams);
|
||||
return await plugin.state.data.applyAction(LoadMvsData, params, ref).run();
|
||||
},
|
||||
});
|
||||
147
src/extensions/mvs/components/multilayer-color-theme.ts
Normal file
147
src/extensions/mvs/components/multilayer-color-theme.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { Bond, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { stringToWords } from '../../../mol-util/string';
|
||||
import { ElementSet, SelectorParams, isSelectorAll } from './selector';
|
||||
|
||||
|
||||
/** Special value that can be used as color with null-like semantic (i.e. "no color provided").
|
||||
* By some lucky coincidence, Mol* treats -1 as white. */
|
||||
export const NoColor = Color(-1);
|
||||
|
||||
/** Return true if `color` is a real color, false if it is `NoColor`. */
|
||||
function isValidColor(color: Color): boolean {
|
||||
return color >= 0;
|
||||
}
|
||||
|
||||
const DefaultBackgroundColor = ColorNames.white;
|
||||
|
||||
/** Parameter definition for color theme "Multilayer" */
|
||||
export function makeMultilayerColorThemeParams(colorThemeRegistry: ColorTheme.Registry, ctx: ThemeDataContext) {
|
||||
const colorThemeInfo = {
|
||||
help: (value: { name: string, params: {} }) => {
|
||||
const { name, params } = value;
|
||||
const p = colorThemeRegistry.get(name);
|
||||
const ct = p.factory({}, params);
|
||||
return { description: ct.description, legend: ct.legend };
|
||||
}
|
||||
};
|
||||
const nestedThemeTypes = colorThemeRegistry.types.filter(([name, label, category]) => name !== MultilayerColorThemeName && colorThemeRegistry.get(name).isApplicable(ctx)); // Adding 'multilayer' theme itself would cause infinite recursion
|
||||
return {
|
||||
layers: PD.ObjectList(
|
||||
{
|
||||
theme: PD.Mapped<any>(
|
||||
'uniform',
|
||||
nestedThemeTypes,
|
||||
name => PD.Group<any>(colorThemeRegistry.get(name).getParams({ structure: Structure.Empty })),
|
||||
colorThemeInfo),
|
||||
selection: SelectorParams,
|
||||
},
|
||||
obj => stringToWords(obj.theme.name),
|
||||
{ description: 'A list of layers, each defining a color theme. The last listed layer is the top layer (applies first). If the top layer does not provide color for a location or its selection does not cover the location, the underneath layers will apply.' }),
|
||||
background: PD.Color(DefaultBackgroundColor, { description: 'Color for elements where no layer applies' }),
|
||||
};
|
||||
}
|
||||
/** Parameter definition for color theme "Multilayer" */
|
||||
export type MultilayerColorThemeParams = ReturnType<typeof makeMultilayerColorThemeParams>
|
||||
|
||||
/** Parameter values for color theme "Multilayer" */
|
||||
export type MultilayerColorThemeProps = PD.Values<MultilayerColorThemeParams>
|
||||
|
||||
/** Default values for `MultilayerColorThemeProps` */
|
||||
export const DefaultMultilayerColorThemeProps: MultilayerColorThemeProps = { layers: [], background: DefaultBackgroundColor };
|
||||
|
||||
|
||||
/** Return color theme that assigns colors based on a list of nested color themes (layers).
|
||||
* The last layer in the list whose selection covers the given location
|
||||
* and which provides a valid (non-negative) color value will be used.
|
||||
* If a nested theme provider has `ensureCustomProperties` methods, these will not be called automatically
|
||||
* (the caller must ensure that any required custom properties be attached). */
|
||||
function makeMultilayerColorTheme(ctx: ThemeDataContext, props: MultilayerColorThemeProps, colorThemeRegistry: ColorTheme.Registry): ColorTheme<MultilayerColorThemeParams> {
|
||||
const colorLayers: { color: LocationColor, elementSet: ElementSet | undefined }[] = []; // undefined elementSet means 'all'
|
||||
for (let i = props.layers.length - 1; i >= 0; i--) { // iterate from end to get top layer first, bottom layer last
|
||||
const layer = props.layers[i];
|
||||
const themeProvider = colorThemeRegistry.get(layer.theme.name);
|
||||
if (!themeProvider) {
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot find it in registry.`);
|
||||
continue;
|
||||
}
|
||||
if (themeProvider.ensureCustomProperties?.attach) {
|
||||
console.warn(`Multilayer color theme: layer "${themeProvider.name}" has ensureCustomProperties.attach method, but Multilayer color theme does not call it. If the layer does not work, make sure you call ensureCustomProperties.attach somewhere.`);
|
||||
}
|
||||
const theme = themeProvider.factory(ctx, layer.theme.params);
|
||||
switch (theme.granularity) {
|
||||
case 'uniform':
|
||||
case 'instance':
|
||||
case 'group':
|
||||
case 'groupInstance':
|
||||
case 'vertex':
|
||||
case 'vertexInstance':
|
||||
const elementSet = isSelectorAll(layer.selection) ? undefined : ElementSet.fromSelector(ctx.structure, layer.selection); // treating 'all' specially for performance reasons (it's expected to be used most often)
|
||||
colorLayers.push({ color: theme.color, elementSet });
|
||||
break;
|
||||
default:
|
||||
console.warn(`Skipping color theme '${layer.theme.name}', cannot process granularity '${theme.granularity}'`);
|
||||
}
|
||||
};
|
||||
|
||||
function structureElementColor(loc: StructureElement.Location, isSecondary: boolean): Color {
|
||||
for (const layer of colorLayers) {
|
||||
const matches = !layer.elementSet || ElementSet.has(layer.elementSet, loc);
|
||||
if (!matches) continue;
|
||||
const color = layer.color(loc, isSecondary);
|
||||
if (!isValidColor(color)) continue;
|
||||
return color;
|
||||
}
|
||||
return props.background;
|
||||
}
|
||||
const auxLocation = StructureElement.Location.create(ctx.structure);
|
||||
|
||||
const color: LocationColor = (location: Location, isSecondary: boolean) => {
|
||||
if (StructureElement.Location.is(location)) {
|
||||
return structureElementColor(location, isSecondary);
|
||||
} else if (Bond.isLocation(location)) {
|
||||
// this will be applied for each bond twice, to get color of each half (a* refers to the adjacent atom, b* to the opposite atom)
|
||||
auxLocation.unit = location.aUnit;
|
||||
auxLocation.element = location.aUnit.elements[location.aIndex];
|
||||
return structureElementColor(auxLocation, isSecondary);
|
||||
}
|
||||
return props.background;
|
||||
};
|
||||
|
||||
return {
|
||||
factory: (ctx_, props_) => makeMultilayerColorTheme(ctx_, props_, colorThemeRegistry),
|
||||
granularity: 'group',
|
||||
preferSmoothing: true,
|
||||
color: color,
|
||||
props: props,
|
||||
description: 'Combines colors from multiple color themes.',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
/** Unique name for "Multilayer" color theme */
|
||||
export const MultilayerColorThemeName = 'mvs-multilayer';
|
||||
|
||||
/** A thingy that is needed to register color theme "Multilayer" */
|
||||
export function makeMultilayerColorThemeProvider(colorThemeRegistry: ColorTheme.Registry): ColorTheme.Provider<MultilayerColorThemeParams, typeof MultilayerColorThemeName> {
|
||||
return {
|
||||
name: MultilayerColorThemeName,
|
||||
label: 'Multi-layer',
|
||||
category: ColorTheme.Category.Misc,
|
||||
factory: (ctx, props) => makeMultilayerColorTheme(ctx, props, colorThemeRegistry),
|
||||
getParams: (ctx: ThemeDataContext) => makeMultilayerColorThemeParams(colorThemeRegistry, ctx),
|
||||
defaultValues: DefaultMultilayerColorThemeProps,
|
||||
isApplicable: (ctx: ThemeDataContext) => true,
|
||||
};
|
||||
}
|
||||
82
src/extensions/mvs/components/selector.ts
Normal file
82
src/extensions/mvs/components/selector.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ElementIndex, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
import { StaticStructureComponentTypes, createStructureComponent } from '../../../mol-plugin-state/helpers/structure-component';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { MolScriptBuilder } from '../../../mol-script/language/builder';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayExtend, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { mapArrayToObject, pickObjectKeys } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { capitalize } from '../../../mol-util/string';
|
||||
import { MVSAnnotationStructureComponentParams, createMVSAnnotationStructureComponent } from './annotation-structure-component';
|
||||
|
||||
|
||||
/** Allowed values for a static selector */
|
||||
export const StaticSelectorChoice = new Choice(mapArrayToObject(StaticStructureComponentTypes, t => capitalize(t)), 'all');
|
||||
export type StaticSelectorChoice = Choice.Values<typeof StaticSelectorChoice>
|
||||
|
||||
|
||||
/** Parameter definition for specifying a part of structure (kinda extension of `StructureComponentParams` from mol-plugin-state/helpers/structure-component) */
|
||||
export const SelectorParams = PD.MappedStatic('static', {
|
||||
static: StaticSelectorChoice.PDSelect(),
|
||||
expression: PD.Value<Expression>(MolScriptBuilder.struct.generator.all),
|
||||
bundle: PD.Value<StructureElement.Bundle>(StructureElement.Bundle.Empty),
|
||||
script: PD.Script({ language: 'mol-script', expression: '(sel.atom.all)' }),
|
||||
annotation: PD.Group(pickObjectKeys(MVSAnnotationStructureComponentParams, ['annotationId', 'fieldName', 'fieldValues'])),
|
||||
}, { description: 'Define a part of the structure where this layer applies (use Static:all to apply to the whole structure)' }
|
||||
);
|
||||
|
||||
/** Parameter values for specifying a part of structure */
|
||||
export type Selector = PD.Values<{ selector: typeof SelectorParams }>['selector']
|
||||
|
||||
/** `Selector` for selecting the whole structure */
|
||||
export const SelectorAll = { name: 'static', params: 'all' } satisfies Selector;
|
||||
|
||||
/** Decide whether a selector is `SelectorAll` */
|
||||
export function isSelectorAll(props: Selector): props is typeof SelectorAll {
|
||||
return props.name === 'static' && props.params === 'all';
|
||||
}
|
||||
|
||||
|
||||
/** Data structure for fast lookup of a structure element location in a substructure */
|
||||
export type ElementSet = { [modelId: UUID]: SortedArray<ElementIndex> }
|
||||
|
||||
export const ElementSet = {
|
||||
/** Create an `ElementSet` from the substructure of `structure` defined by `selector` */
|
||||
fromSelector(structure: Structure | undefined, selector: Selector): ElementSet {
|
||||
if (!structure) return {};
|
||||
const arrays: { [modelId: UUID]: ElementIndex[] } = {};
|
||||
const selection = substructureFromSelector(structure, selector); // using `getAtomRangesForRow` might (might not) be faster here
|
||||
for (const unit of selection.units) {
|
||||
arrayExtend(arrays[unit.model.id] ??= [], unit.elements);
|
||||
}
|
||||
const result: { [modelId: UUID]: SortedArray<ElementIndex> } = {};
|
||||
for (const modelId in arrays) {
|
||||
const array = arrays[modelId as UUID];
|
||||
sortIfNeeded(array, (a, b) => a - b);
|
||||
result[modelId as UUID] = SortedArray.ofSortedArray(array);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
/** Decide if the element set `set` contains structure element location `location` */
|
||||
has(set: ElementSet, location: StructureElement.Location): boolean {
|
||||
const array = set[location.unit.model.id];
|
||||
return array ? SortedArray.has(array, location.element) : false;
|
||||
},
|
||||
};
|
||||
|
||||
/** Return a substructure of `structure` defined by `selector` */
|
||||
export function substructureFromSelector(structure: Structure, selector: Selector): Structure {
|
||||
const pso = (selector.name === 'annotation') ?
|
||||
createMVSAnnotationStructureComponent(structure, { ...selector.params, label: '', nullIfEmpty: false })
|
||||
: createStructureComponent(structure, { type: selector, label: '', nullIfEmpty: false }, { source: structure });
|
||||
return PluginStateObject.Molecule.Structure.is(pso) ? pso.data : Structure.Empty;
|
||||
}
|
||||
50
src/extensions/mvs/helpers/_spec/atom-ranges.spec.ts
Normal file
50
src/extensions/mvs/helpers/_spec/atom-ranges.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { AtomRanges } from '../atom-ranges';
|
||||
|
||||
|
||||
describe('union', () => {
|
||||
it('union non-overlapping', async () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
const b = {
|
||||
from: [11, 37, 51, 205],
|
||||
to: [15, 39, 55, 210],
|
||||
} as AtomRanges;
|
||||
const c = {
|
||||
from: [-10, 200, 300],
|
||||
to: [-5, 202, 305],
|
||||
} as AtomRanges;
|
||||
const result = {
|
||||
from: [-10, 0, 11, 20, 37, 40, 51, 60, 80, 200, 205, 300],
|
||||
to: [-5, 10, 15, 30, 39, 50, 55, 70, 90, 202, 210, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
it('union overlapping', async () => {
|
||||
const a = {
|
||||
from: [0, 20, 40, 60, 80],
|
||||
to: [10, 30, 50, 70, 90],
|
||||
} as AtomRanges;
|
||||
const b = {
|
||||
from: [10, 37, 51, 84, 205],
|
||||
to: [15, 40, 55, 88, 220],
|
||||
} as AtomRanges;
|
||||
const c = {
|
||||
from: [-10, 67, 200, 300],
|
||||
to: [5, 80, 210, 305],
|
||||
} as AtomRanges;
|
||||
const result = {
|
||||
from: [-10, 20, 37, 51, 60, 200, 300],
|
||||
to: [15, 30, 50, 55, 90, 220, 305],
|
||||
} as AtomRanges;
|
||||
expect(AtomRanges.union([a, b, c])).toEqual(result);
|
||||
});
|
||||
});
|
||||
|
||||
29
src/extensions/mvs/helpers/_spec/selections.spec.ts
Normal file
29
src/extensions/mvs/helpers/_spec/selections.spec.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { range } from '../../../../mol-util/array';
|
||||
import { MVSAnnotationRow } from '../schemas';
|
||||
import { groupRows } from '../selections';
|
||||
|
||||
|
||||
describe('groupRows', () => {
|
||||
it('groupRows', async () => {
|
||||
const rows = [
|
||||
{ label: 'A' }, { label: 'B', group_id: 1 }, { label: 'C', group_id: 'x' }, { label: 'D', group_id: 1 },
|
||||
{ label: 'E' }, { label: 'F' }, { label: 'G', group_id: 'x' }, { label: 'H', group_id: 'x' },
|
||||
] as any as MVSAnnotationRow[];
|
||||
const g = groupRows(rows);
|
||||
const groupedIndices = range(g.count).map(i => g.grouped.slice(g.offsets[i], g.offsets[i + 1]));
|
||||
const groupedRows = groupedIndices.map(group => group.map(j => rows[j]));
|
||||
expect(groupedRows).toEqual([
|
||||
[{ label: 'A' }],
|
||||
[{ label: 'B', group_id: 1 }, { label: 'D', group_id: 1 }],
|
||||
[{ label: 'C', group_id: 'x' }, { label: 'G', group_id: 'x' }, { label: 'H', group_id: 'x' }],
|
||||
[{ label: 'E' }],
|
||||
[{ label: 'F' }],
|
||||
]);
|
||||
});
|
||||
});
|
||||
137
src/extensions/mvs/helpers/atom-ranges.ts
Normal file
137
src/extensions/mvs/helpers/atom-ranges.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ElementIndex } from '../../../mol-model/structure';
|
||||
import { arrayExtend, range } from '../../../mol-util/array';
|
||||
|
||||
|
||||
/** Represents a collection of disjoint atom ranges in a model.
|
||||
* The number of ranges is `AtomRanges.count(ranges)`,
|
||||
* the i-th range covers atoms `[ranges.from[i], ranges.to[i])`. */
|
||||
export interface AtomRanges {
|
||||
from: ElementIndex[],
|
||||
to: ElementIndex[],
|
||||
}
|
||||
|
||||
export const AtomRanges = {
|
||||
/** Return the number of disjoined ranges in a `AtomRanges` object */
|
||||
count(ranges: AtomRanges): number {
|
||||
return ranges.from.length;
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` without any atoms */
|
||||
empty(): AtomRanges {
|
||||
return { from: [], to: [] };
|
||||
},
|
||||
|
||||
/** Create new `AtomRanges` containing a single range of atoms `[from, to)` */
|
||||
single(from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
return { from: [from], to: [to] };
|
||||
},
|
||||
|
||||
/** Add a range of atoms `[from, to)` to existing `AtomRanges` and return the modified original.
|
||||
* The added range must start after the end of the last existing range
|
||||
* (if it starts just on the next atom, these two ranges will get merged). */
|
||||
add(ranges: AtomRanges, from: ElementIndex, to: ElementIndex): AtomRanges {
|
||||
const n = AtomRanges.count(ranges);
|
||||
if (n > 0) {
|
||||
const lastTo = ranges.to[n - 1];
|
||||
if (from < lastTo) throw new Error('Overlapping ranges not allowed');
|
||||
if (from === lastTo) {
|
||||
ranges.to[n - 1] = to;
|
||||
} else {
|
||||
ranges.from.push(from);
|
||||
ranges.to.push(to);
|
||||
}
|
||||
} else {
|
||||
ranges.from.push(from);
|
||||
ranges.to.push(to);
|
||||
}
|
||||
return ranges;
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` */
|
||||
foreach(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => any) {
|
||||
const n = AtomRanges.count(ranges);
|
||||
for (let i = 0; i < n; i++) func(ranges.from[i], ranges.to[i]);
|
||||
},
|
||||
|
||||
/** Apply function `func` to each range in `ranges` and return an array with results */
|
||||
map<T>(ranges: AtomRanges, func: (from: ElementIndex, to: ElementIndex) => T): T[] {
|
||||
const n = AtomRanges.count(ranges);
|
||||
const result: T[] = new Array(n);
|
||||
for (let i = 0; i < n; i++) result[i] = func(ranges.from[i], ranges.to[i]);
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Compute the set union of multiple `AtomRanges` objects (as sets of atoms) */
|
||||
union(ranges: AtomRanges[]): AtomRanges {
|
||||
const concat = AtomRanges.empty();
|
||||
for (const r of ranges) {
|
||||
arrayExtend(concat.from, r.from);
|
||||
arrayExtend(concat.to, r.to);
|
||||
}
|
||||
const indices = range(concat.from.length).sort((i, j) => concat.from[i] - concat.from[j]); // sort by start of range
|
||||
const result = AtomRanges.empty();
|
||||
let last = -1;
|
||||
for (const i of indices) {
|
||||
const from = concat.from[i];
|
||||
const to = concat.to[i];
|
||||
if (last >= 0 && from <= result.to[last]) {
|
||||
if (to > result.to[last]) {
|
||||
result.to[last] = to;
|
||||
}
|
||||
} else {
|
||||
result.from.push(from);
|
||||
result.to.push(to);
|
||||
last++;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Return a sorted subset of `atoms` which lie in any of `ranges` (i.e. set intersection of `atoms` and `ranges`).
|
||||
* If `out` is provided, use it to store the result (clear any old contents).
|
||||
* If `outFirstAtomIndex` is provided, fill `outFirstAtomIndex.value` with the index of the first selected atom (if any). */
|
||||
selectAtomsInRanges(atoms: SortedArray<ElementIndex>, ranges: AtomRanges, out?: ElementIndex[], outFirstAtomIndex: { value?: number } = {}): ElementIndex[] {
|
||||
out ??= [];
|
||||
out.length = 0;
|
||||
outFirstAtomIndex.value = undefined;
|
||||
|
||||
const nAtoms = atoms.length;
|
||||
const nRanges = AtomRanges.count(ranges);
|
||||
if (nAtoms <= nRanges) {
|
||||
// Implementation 1 (more efficient when there are fewer atoms)
|
||||
let iRange = SortedArray.findPredecessorIndex(SortedArray.ofSortedArray(ranges.to), atoms[0] + 1);
|
||||
for (let iAtom = 0; iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
while (iRange < nRanges && ranges.to[iRange] <= a) iRange++;
|
||||
const qualifies = iRange < nRanges && ranges.from[iRange] <= a;
|
||||
if (qualifies) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Implementation 2 (more efficient when there are fewer ranges)
|
||||
for (let iRange = 0; iRange < nRanges; iRange++) {
|
||||
const from = ranges.from[iRange];
|
||||
const to = ranges.to[iRange];
|
||||
for (let iAtom = SortedArray.findPredecessorIndex(atoms, from); iAtom < nAtoms; iAtom++) {
|
||||
const a = atoms[iAtom];
|
||||
if (a < to) {
|
||||
out.push(a);
|
||||
outFirstAtomIndex.value ??= iAtom;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
},
|
||||
};
|
||||
130
src/extensions/mvs/helpers/indexing.ts
Normal file
130
src/extensions/mvs/helpers/indexing.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
|
||||
import { filterInPlace, range, sortIfNeeded } from '../../../mol-util/array';
|
||||
import { Mapping, MultiMap, NumberMap } from './utils';
|
||||
|
||||
|
||||
/** Auxiliary data structure for efficiently finding chains/residues/atoms in a model by their properties */
|
||||
export interface IndicesAndSortings {
|
||||
chainsByLabelEntityId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByLabelAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
chainsByAuthAsymId: Mapping<string, readonly ChainIndex[]>,
|
||||
residuesSortedByLabelSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesSortedByAuthSeqId: Mapping<ChainIndex, Sorting<ResidueIndex, number>>,
|
||||
residuesByInsCode: Mapping<ChainIndex, Mapping<string, readonly ResidueIndex[]>>,
|
||||
atomsById: Mapping<number, ElementIndex>,
|
||||
atomsByIndex: Mapping<number, ElementIndex>,
|
||||
}
|
||||
|
||||
export const IndicesAndSortings = {
|
||||
/** Get `IndicesAndSortings` for a model (use a cached value or create if not available yet) */
|
||||
get(model: Model): IndicesAndSortings {
|
||||
return model._dynamicPropertyData['indices-and-sortings'] ??= IndicesAndSortings.create(model);
|
||||
},
|
||||
|
||||
/** Create `IndicesAndSortings` for a model */
|
||||
create(model: Model): IndicesAndSortings {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
const nChains = h.chains._rowCount;
|
||||
const { label_entity_id, label_asym_id, auth_asym_id } = h.chains;
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = h.residues;
|
||||
const { Present } = Column.ValueKind;
|
||||
|
||||
const chainsByLabelEntityId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByLabelAsymId = new MultiMap<string, ChainIndex>();
|
||||
const chainsByAuthAsymId = new MultiMap<string, ChainIndex>();
|
||||
const residuesSortedByLabelSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesSortedByAuthSeqId = new Map<ChainIndex, Sorting<ResidueIndex, number>>();
|
||||
const residuesByInsCode = new Map<ChainIndex, MultiMap<string, ResidueIndex>>();
|
||||
const atomsById = new NumberMap<number, ElementIndex>(nAtoms + 1);
|
||||
const atomsByIndex = new NumberMap<number, ElementIndex>(nAtoms);
|
||||
|
||||
for (let iChain = 0 as ChainIndex; iChain < nChains; iChain++) {
|
||||
chainsByLabelEntityId.add(label_entity_id.value(iChain), iChain);
|
||||
chainsByLabelAsymId.add(label_asym_id.value(iChain), iChain);
|
||||
chainsByAuthAsymId.add(auth_asym_id.value(iChain), iChain);
|
||||
|
||||
const iResFrom = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain]];
|
||||
const iResTo = h.residueAtomSegments.index[h.chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
|
||||
const residuesWithLabelSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => label_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByLabelSeqId.set(iChain, Sorting.create(residuesWithLabelSeqId, label_seq_id.value));
|
||||
|
||||
const residuesWithAuthSeqId = filterInPlace(range(iResFrom, iResTo) as ResidueIndex[], iRes => auth_seq_id.valueKind(iRes) === Present);
|
||||
residuesSortedByAuthSeqId.set(iChain, Sorting.create(residuesWithAuthSeqId, auth_seq_id.value));
|
||||
|
||||
const residuesHereByInsCode = new MultiMap<string, ResidueIndex>();
|
||||
for (let iRes = iResFrom; iRes < iResTo; iRes++) {
|
||||
if (pdbx_PDB_ins_code.valueKind(iRes) === Present) {
|
||||
residuesHereByInsCode.add(pdbx_PDB_ins_code.value(iRes), iRes);
|
||||
}
|
||||
}
|
||||
residuesByInsCode.set(iChain, residuesHereByInsCode);
|
||||
}
|
||||
|
||||
const atomId = model.atomicConformation.atomId.value;
|
||||
const atomIndex = h.atomSourceIndex.value;
|
||||
for (let iAtom = 0 as ElementIndex; iAtom < nAtoms; iAtom++) {
|
||||
atomsById.set(atomId(iAtom), iAtom);
|
||||
atomsByIndex.set(atomIndex(iAtom), iAtom);
|
||||
}
|
||||
|
||||
return {
|
||||
chainsByLabelEntityId, chainsByLabelAsymId, chainsByAuthAsymId,
|
||||
residuesSortedByLabelSeqId, residuesSortedByAuthSeqId, residuesByInsCode,
|
||||
atomsById, atomsByIndex,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** Represents a set of things (keys) of type `K`, sorted by some property (value) of type `V` */
|
||||
export interface Sorting<K, V extends number> {
|
||||
/** Keys sorted by their corresponding values */
|
||||
keys: readonly K[],
|
||||
/** Sorted values corresponding to each key (value for `keys[i]` is `values[i]`) */
|
||||
values: SortedArray<V>,
|
||||
}
|
||||
|
||||
export const Sorting = {
|
||||
/** Create a `Sorting` from an array of keys and a function returning their corresponding values.
|
||||
* If two keys have the same value, the smaller key will come first.
|
||||
* This function modifies `keys` - create a copy if you need the original order! */
|
||||
create<K extends number, V extends number>(keys: K[], valueFunction: (k: K) => V): Sorting<K, V> {
|
||||
sortIfNeeded(keys, (a, b) => valueFunction(a) - valueFunction(b) || a - b);
|
||||
const values: SortedArray<V> = SortedArray.ofSortedArray(keys.map(valueFunction));
|
||||
return { keys, values };
|
||||
},
|
||||
|
||||
/** Return a newly allocated array of keys which have value equal to `target`.
|
||||
* The returned keys are sorted by their value. */
|
||||
getKeysWithValue<K, V extends number>(sorting: Sorting<K, V>, target: V): K[] {
|
||||
return Sorting.getKeysWithValueInRange(sorting, target, target);
|
||||
},
|
||||
|
||||
/** Return a newly allocated array of keys which have value within interval `[min, max]` (inclusive).
|
||||
* The returned keys are sorted by their value.
|
||||
* Undefined `min` is interpreted as negative infitity, undefined `max` is interpreted as positive infinity. */
|
||||
getKeysWithValueInRange<K, V extends number>(sorting: Sorting<K, V>, min: V | undefined, max: V | undefined): K[] {
|
||||
const { keys, values } = sorting;
|
||||
if (!keys) return [];
|
||||
const n = keys.length;
|
||||
const from = (min !== undefined) ? SortedArray.findPredecessorIndex(values, min) : 0;
|
||||
let to: number;
|
||||
if (max !== undefined) {
|
||||
to = from;
|
||||
while (to < n && values[to] <= max) to++;
|
||||
} else {
|
||||
to = n;
|
||||
}
|
||||
return keys.slice(from, to);
|
||||
},
|
||||
};
|
||||
87
src/extensions/mvs/helpers/label-text.ts
Normal file
87
src/extensions/mvs/helpers/label-text.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { BoundaryHelper } from '../../../mol-math/geometry/boundary-helper';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra';
|
||||
import { ElementIndex, Model, Structure, StructureElement, StructureProperties } from '../../../mol-model/structure';
|
||||
import { UUID } from '../../../mol-util';
|
||||
import { arrayExtend } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { IndicesAndSortings } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { getAtomRangesForRows } from './selections';
|
||||
|
||||
|
||||
/** Properties describing position, size, etc. of a text in 3D */
|
||||
export interface TextProps {
|
||||
/** Anchor point for the text (i.e. the center of the text will appear in front of `center`) */
|
||||
center: Vec3,
|
||||
/** Depth of the text wrt anchor point (i.e. the text will appear in distance `radius` in front of the anchor point) */
|
||||
depth: number,
|
||||
/** Relative text size */
|
||||
scale: number,
|
||||
/** Index of the first atom within structure, to which this text is bound (for coloring and similar purposes) */
|
||||
group: number,
|
||||
}
|
||||
|
||||
const tmpVec = Vec3();
|
||||
const tmpArray: number[] = [];
|
||||
const boundaryHelper = new BoundaryHelper('98');
|
||||
const outAtoms: ElementIndex[] = [];
|
||||
const outFirstAtomIndex: { value?: number } = {};
|
||||
|
||||
/** Return `TextProps` (position, size, etc.) for a text that is to be bound to a substructure of `structure` defined by union of `rows`.
|
||||
* Derives `center` and `depth` from the boundary sphere of the substructure, `scale` from the number of heavy atoms in the substructure. */
|
||||
export function textPropsForSelection(structure: Structure, sizeFunction: (location: StructureElement.Location) => number, rows: MVSAnnotationRow | MVSAnnotationRow[], onlyInModel?: Model): TextProps | undefined {
|
||||
const loc = StructureElement.Location.create(structure);
|
||||
const { units } = structure;
|
||||
const { type_symbol } = StructureProperties.atom;
|
||||
tmpArray.length = 0;
|
||||
let includedAtoms = 0;
|
||||
let includedHeavyAtoms = 0;
|
||||
let group: number | undefined = undefined;
|
||||
let atomSize: number | undefined = undefined;
|
||||
const rangesByModel: { [modelId: UUID]: AtomRanges } = {};
|
||||
for (let iUnit = 0, nUnits = units.length; iUnit < nUnits; iUnit++) {
|
||||
const unit = units[iUnit];
|
||||
if (onlyInModel && unit.model.id !== onlyInModel.id) continue;
|
||||
const ranges = rangesByModel[unit.model.id] ??= getAtomRangesForRows(unit.model, rows, IndicesAndSortings.get(unit.model));
|
||||
const position = unit.conformation.position;
|
||||
loc.unit = unit;
|
||||
AtomRanges.selectAtomsInRanges(unit.elements, ranges, outAtoms, outFirstAtomIndex);
|
||||
for (const atom of outAtoms) {
|
||||
loc.element = atom;
|
||||
position(atom, tmpVec);
|
||||
arrayExtend(tmpArray, tmpVec);
|
||||
group ??= structure.serialMapping.cumulativeUnitElementCount[iUnit] + outFirstAtomIndex.value!;
|
||||
atomSize ??= sizeFunction(loc);
|
||||
includedAtoms++;
|
||||
if (type_symbol(loc) !== 'H') includedHeavyAtoms++;
|
||||
}
|
||||
}
|
||||
if (includedAtoms > 0) {
|
||||
const { center, radius } = (includedAtoms > 1) ? boundarySphere(tmpArray) : { center: Vec3.fromArray(Vec3(), tmpArray, 0), radius: 1.1 * atomSize! };
|
||||
const scale = (includedHeavyAtoms || includedAtoms) ** (1 / 3);
|
||||
return { center, depth: radius, scale, group: group! };
|
||||
}
|
||||
}
|
||||
|
||||
/** Calculate the boundary sphere for a set of points given by their flattened coordinates (`flatCoords.slice(0,3)` is the first point etc.) */
|
||||
function boundarySphere(flatCoords: readonly number[]): Sphere3D {
|
||||
const length = flatCoords.length;
|
||||
boundaryHelper.reset();
|
||||
for (let offset = 0; offset < length; offset += 3) {
|
||||
Vec3.fromArray(tmpVec, flatCoords, offset);
|
||||
boundaryHelper.includePosition(tmpVec);
|
||||
}
|
||||
boundaryHelper.finishedIncludeStep();
|
||||
for (let offset = 0; offset < length; offset += 3) {
|
||||
Vec3.fromArray(tmpVec, flatCoords, offset);
|
||||
boundaryHelper.radiusPosition(tmpVec);
|
||||
}
|
||||
return boundaryHelper.getSphere();
|
||||
}
|
||||
35
src/extensions/mvs/helpers/param-definition.ts
Normal file
35
src/extensions/mvs/helpers/param-definition.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
|
||||
/** Similar to `PD.Numeric` but allows leaving empty field in UI (treated as `undefined`) */
|
||||
export function MaybeIntegerParamDefinition(defaultValue?: number, info?: PD.Info): PD.Base<number | undefined> {
|
||||
return PD.Converted<number | undefined, PD.Text>(stringifyMaybeInt, parseMaybeInt, PD.Text(stringifyMaybeInt(defaultValue), info));
|
||||
}
|
||||
/** The magic with negative zero looks crazy, but it's needed if we want to be able to write negative numbers, LOL. Please help if you know a better solution. */
|
||||
function parseMaybeInt(input: string): number | undefined {
|
||||
if (input.trim() === '-') return -0;
|
||||
const num = parseInt(input);
|
||||
return isNaN(num) ? undefined : num;
|
||||
}
|
||||
function stringifyMaybeInt(num: number | undefined): string {
|
||||
if (num === undefined) return '';
|
||||
if (Object.is(num, -0)) return '-';
|
||||
return num.toString();
|
||||
}
|
||||
|
||||
/** Similar to `PD.Text` but leaving empty field in UI is treated as `undefined` */
|
||||
export function MaybeStringParamDefinition(defaultValue?: string, info?: PD.Info): PD.Base<string | undefined> {
|
||||
return PD.Converted<string | undefined, PD.Text>(stringifyMaybeString, parseMaybeString, PD.Text(stringifyMaybeString(defaultValue), info));
|
||||
}
|
||||
function parseMaybeString(input: string): string | undefined {
|
||||
return input === '' ? undefined : input;
|
||||
}
|
||||
function stringifyMaybeString(str: string | undefined): string {
|
||||
return str === undefined ? '' : str;
|
||||
}
|
||||
92
src/extensions/mvs/helpers/schemas.ts
Normal file
92
src/extensions/mvs/helpers/schemas.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column, Table } from '../../../mol-data/db';
|
||||
import { pickObjectKeys } from '../../../mol-util/object';
|
||||
import { Choice } from '../../../mol-util/param-choice';
|
||||
|
||||
const { str, int } = Column.Schema;
|
||||
|
||||
|
||||
/** Names of allowed MVS annotation schemas (values for the annotation schema parameter) */
|
||||
export type MVSAnnotationSchema = Choice.Values<typeof MVSAnnotationSchema>
|
||||
export const MVSAnnotationSchema = new Choice(
|
||||
{
|
||||
whole_structure: 'Whole Structure',
|
||||
entity: 'Entity',
|
||||
chain: 'Chain (label*)',
|
||||
auth_chain: 'Chain (auth*)',
|
||||
residue: 'Residue (label*)',
|
||||
auth_residue: 'Residue (auth*)',
|
||||
residue_range: 'Residue range (label*)',
|
||||
auth_residue_range: 'Residue range (auth*)',
|
||||
atom: 'Atom (label*)',
|
||||
auth_atom: 'Atom (auth*)',
|
||||
all_atomic: 'All atomic selectors',
|
||||
},
|
||||
'all_atomic',
|
||||
);
|
||||
|
||||
/** Represents a set of criteria for selection of atoms in a model (in `all_atomic` schema).
|
||||
* Missing/undefined values mean that we do not care about that specific atom property. */
|
||||
export type MVSAnnotationRow = Partial<Table.Row<typeof AllAtomicCifAnnotationSchema>>
|
||||
|
||||
|
||||
/** Get CIF schema definition for given annotation schema name */
|
||||
export function getCifAnnotationSchema<K extends MVSAnnotationSchema>(schemaName: K): Pick<typeof AllAtomicCifAnnotationSchema, (typeof FieldsForSchemas)[K][number]> {
|
||||
return pickObjectKeys(AllAtomicCifAnnotationSchema, FieldsForSchemas[schemaName]);
|
||||
}
|
||||
|
||||
|
||||
/** Definition of `all_atomic` schema for CIF (other atomic schemas are subschemas of this one) */
|
||||
const AllAtomicCifAnnotationSchema = {
|
||||
/** Tag for grouping multiple annotation rows with the same `group_id` (e.g. to show one label for two chains);
|
||||
* if the `group_id` is not given, each row is processed separately */
|
||||
group_id: str,
|
||||
|
||||
label_entity_id: str,
|
||||
label_asym_id: str,
|
||||
auth_asym_id: str,
|
||||
|
||||
label_seq_id: int,
|
||||
auth_seq_id: int,
|
||||
pdbx_PDB_ins_code: str,
|
||||
/** Minimum label_seq_id (inclusive) */
|
||||
beg_label_seq_id: int,
|
||||
/** Maximum label_seq_id (inclusive) */
|
||||
end_label_seq_id: int,
|
||||
/** Minimum auth_seq_id (inclusive) */
|
||||
beg_auth_seq_id: int,
|
||||
/** Maximum auth_seq_id (inclusive) */
|
||||
end_auth_seq_id: int,
|
||||
|
||||
/** Atom name like 'CA', 'N', 'O'... */
|
||||
label_atom_id: str,
|
||||
/** Atom name like 'CA', 'N', 'O'... */
|
||||
auth_atom_id: str,
|
||||
/** Element symbol like 'H', 'He', 'Li', 'Be' (case-insensitive)... */
|
||||
type_symbol: str,
|
||||
/** Unique atom identifier across conformations (_atom_site.id) */
|
||||
atom_id: int,
|
||||
/** 0-based index of the atom in the source data */
|
||||
atom_index: int,
|
||||
} satisfies Table.Schema;
|
||||
|
||||
/** Allowed fields (i.e. CIF columns or JSON keys) for each annotation schema
|
||||
* (other fields will just be ignored) */
|
||||
const FieldsForSchemas = {
|
||||
whole_structure: ['group_id'],
|
||||
entity: ['group_id', 'label_entity_id'],
|
||||
chain: ['group_id', 'label_entity_id', 'label_asym_id'],
|
||||
auth_chain: ['group_id', 'auth_asym_id'],
|
||||
residue: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id'],
|
||||
auth_residue: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code'],
|
||||
residue_range: ['group_id', 'label_entity_id', 'label_asym_id', 'beg_label_seq_id', 'end_label_seq_id'],
|
||||
auth_residue_range: ['group_id', 'auth_asym_id', 'beg_auth_seq_id', 'end_auth_seq_id'],
|
||||
atom: ['group_id', 'label_entity_id', 'label_asym_id', 'label_seq_id', 'label_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
auth_atom: ['group_id', 'auth_asym_id', 'auth_seq_id', 'pdbx_PDB_ins_code', 'auth_atom_id', 'type_symbol', 'atom_id', 'atom_index'],
|
||||
all_atomic: Object.keys(AllAtomicCifAnnotationSchema) as (keyof typeof AllAtomicCifAnnotationSchema)[],
|
||||
} satisfies { [schema in MVSAnnotationSchema]: (keyof typeof AllAtomicCifAnnotationSchema)[] };
|
||||
361
src/extensions/mvs/helpers/selections.ts
Normal file
361
src/extensions/mvs/helpers/selections.ts
Normal file
@@ -0,0 +1,361 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Column } from '../../../mol-data/db';
|
||||
import { ChainIndex, ElementIndex, Model, ResidueIndex } from '../../../mol-model/structure';
|
||||
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
|
||||
import { Expression } from '../../../mol-script/language/expression';
|
||||
import { arrayExtend, filterInPlace, range } from '../../../mol-util/array';
|
||||
import { AtomRanges } from './atom-ranges';
|
||||
import { IndicesAndSortings, Sorting } from './indexing';
|
||||
import { MVSAnnotationRow } from './schemas';
|
||||
import { isAnyDefined, isDefined } from './utils';
|
||||
|
||||
|
||||
const EmptyArray: readonly any[] = [];
|
||||
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by `row` */
|
||||
export function getAtomRangesForRow(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): AtomRanges {
|
||||
const h = model.atomicHierarchy;
|
||||
const nAtoms = h.atoms._rowCount;
|
||||
|
||||
const hasAtomIds = isAnyDefined(row.atom_id, row.atom_index);
|
||||
const hasAtomFilter = isAnyDefined(row.label_atom_id, row.auth_atom_id, row.type_symbol);
|
||||
const hasResidueFilter = isAnyDefined(row.label_seq_id, row.auth_seq_id, row.pdbx_PDB_ins_code, row.beg_label_seq_id, row.end_label_seq_id, row.beg_auth_seq_id, row.end_auth_seq_id);
|
||||
const hasChainFilter = isAnyDefined(row.label_asym_id, row.auth_asym_id, row.label_entity_id);
|
||||
|
||||
if (hasAtomIds) {
|
||||
const theAtom = getTheAtomForRow(model, row, indices);
|
||||
return theAtom !== undefined ? AtomRanges.single(theAtom, theAtom + 1 as ElementIndex) : AtomRanges.empty();
|
||||
}
|
||||
|
||||
if (!hasChainFilter && !hasResidueFilter && !hasAtomFilter) {
|
||||
return AtomRanges.single(0 as ElementIndex, nAtoms as ElementIndex);
|
||||
}
|
||||
|
||||
const qualifyingChains = getQualifyingChains(model, row, indices);
|
||||
if (!hasResidueFilter && !hasAtomFilter) {
|
||||
const chainOffsets = h.chainAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
for (const iChain of qualifyingChains) {
|
||||
AtomRanges.add(ranges, chainOffsets[iChain], chainOffsets[iChain + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingResidues = getQualifyingResidues(model, row, indices, qualifyingChains);
|
||||
if (!hasAtomFilter) {
|
||||
const residueOffsets = h.residueAtomSegments.offsets;
|
||||
const ranges = AtomRanges.empty();
|
||||
for (const iRes of qualifyingResidues) {
|
||||
AtomRanges.add(ranges, residueOffsets[iRes], residueOffsets[iRes + 1]);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
const qualifyingAtoms = getQualifyingAtoms(model, row, indices, qualifyingResidues);
|
||||
const ranges = AtomRanges.empty();
|
||||
for (const iAtom of qualifyingAtoms) {
|
||||
AtomRanges.add(ranges, iAtom, iAtom + 1 as ElementIndex);
|
||||
}
|
||||
return ranges;
|
||||
}
|
||||
|
||||
/** Return atom ranges in `model` which satisfy criteria given by any of `rows` (atoms that satisfy more rows are still included only once) */
|
||||
export function getAtomRangesForRows(model: Model, rows: MVSAnnotationRow | MVSAnnotationRow[], indices: IndicesAndSortings): AtomRanges {
|
||||
if (Array.isArray(rows)) {
|
||||
return AtomRanges.union(rows.map(row => getAtomRangesForRow(model, row, indices)));
|
||||
} else {
|
||||
return getAtomRangesForRow(model, rows, indices);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Return an array of chain indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingChains(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): readonly ChainIndex[] {
|
||||
const { auth_asym_id, label_entity_id, _rowCount: nChains } = model.atomicHierarchy.chains;
|
||||
let result: readonly ChainIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_asym_id)) {
|
||||
result = indices.chainsByLabelAsymId.get(row.label_asym_id) ?? EmptyArray;
|
||||
}
|
||||
if (isDefined(row.auth_asym_id)) {
|
||||
if (result) {
|
||||
result = result.filter(i => auth_asym_id.value(i) === row.auth_asym_id);
|
||||
} else {
|
||||
result = indices.chainsByAuthAsymId.get(row.auth_asym_id) ?? EmptyArray;
|
||||
}
|
||||
}
|
||||
if (isDefined(row.label_entity_id)) {
|
||||
if (result) {
|
||||
result = result.filter(i => label_entity_id.value(i) === row.label_entity_id);
|
||||
} else {
|
||||
result = indices.chainsByLabelEntityId.get(row.label_entity_id) ?? EmptyArray;
|
||||
}
|
||||
}
|
||||
result ??= range(nChains) as ChainIndex[];
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of residue indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingResidues(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromChains: readonly ChainIndex[]): ResidueIndex[] {
|
||||
const { label_seq_id, auth_seq_id, pdbx_PDB_ins_code } = model.atomicHierarchy.residues;
|
||||
const { Present } = Column.ValueKind;
|
||||
const result: ResidueIndex[] = [];
|
||||
for (const iChain of fromChains) {
|
||||
let residuesHere: readonly ResidueIndex[] | undefined = undefined;
|
||||
if (isDefined(row.label_seq_id)) {
|
||||
const sorting = indices.residuesSortedByLabelSeqId.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.label_seq_id);
|
||||
}
|
||||
if (isDefined(row.auth_seq_id)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => auth_seq_id.valueKind(i) === Present && auth_seq_id.value(i) === row.auth_seq_id);
|
||||
} else {
|
||||
const sorting = indices.residuesSortedByAuthSeqId.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValue(sorting, row.auth_seq_id);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.pdbx_PDB_ins_code)) {
|
||||
if (residuesHere) {
|
||||
residuesHere = residuesHere.filter(i => pdbx_PDB_ins_code.value(i) === row.pdbx_PDB_ins_code);
|
||||
} else {
|
||||
residuesHere = indices.residuesByInsCode.get(iChain)!.get(row.pdbx_PDB_ins_code) ?? EmptyArray;
|
||||
}
|
||||
}
|
||||
if (isDefined(row.beg_label_seq_id) || isDefined(row.end_label_seq_id)) {
|
||||
if (residuesHere) {
|
||||
if (isDefined(row.beg_label_seq_id)) {
|
||||
residuesHere = residuesHere.filter(i => label_seq_id.valueKind(i) === Present && label_seq_id.value(i) >= row.beg_label_seq_id!);
|
||||
}
|
||||
if (isDefined(row.end_label_seq_id)) {
|
||||
residuesHere = residuesHere.filter(i => label_seq_id.valueKind(i) === Present && label_seq_id.value(i) <= row.end_label_seq_id!);
|
||||
}
|
||||
} else {
|
||||
const sorting = indices.residuesSortedByLabelSeqId.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValueInRange(sorting, row.beg_label_seq_id, row.end_label_seq_id);
|
||||
}
|
||||
}
|
||||
if (isDefined(row.beg_auth_seq_id) || isDefined(row.end_auth_seq_id)) {
|
||||
if (residuesHere) {
|
||||
if (isDefined(row.beg_auth_seq_id)) {
|
||||
residuesHere = residuesHere.filter(i => auth_seq_id.valueKind(i) === Present && auth_seq_id.value(i) >= row.beg_auth_seq_id!);
|
||||
}
|
||||
if (isDefined(row.end_auth_seq_id)) {
|
||||
residuesHere = residuesHere.filter(i => auth_seq_id.valueKind(i) === Present && auth_seq_id.value(i) <= row.end_auth_seq_id!);
|
||||
}
|
||||
} else {
|
||||
const sorting = indices.residuesSortedByAuthSeqId.get(iChain)!;
|
||||
residuesHere = Sorting.getKeysWithValueInRange(sorting, row.beg_auth_seq_id, row.end_auth_seq_id);
|
||||
}
|
||||
}
|
||||
if (!residuesHere) {
|
||||
const { residueAtomSegments, chainAtomSegments } = model.atomicHierarchy;
|
||||
const firstResidueForChain = residueAtomSegments.index[chainAtomSegments.offsets[iChain]];
|
||||
const firstResidueAfterChain = residueAtomSegments.index[chainAtomSegments.offsets[iChain + 1] - 1] + 1;
|
||||
residuesHere = range(firstResidueForChain, firstResidueAfterChain) as ResidueIndex[];
|
||||
}
|
||||
arrayExtend(result, residuesHere);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return an array of atom indexes which satisfy criteria given by `row` */
|
||||
function getQualifyingAtoms(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings, fromResidues: readonly ResidueIndex[]): ElementIndex[] {
|
||||
const { label_atom_id, auth_atom_id, type_symbol } = model.atomicHierarchy.atoms;
|
||||
const residueAtomSegments_offsets = model.atomicHierarchy.residueAtomSegments.offsets;
|
||||
const result: ElementIndex[] = [];
|
||||
for (const iRes of fromResidues) {
|
||||
const atomIdcs = range(residueAtomSegments_offsets[iRes], residueAtomSegments_offsets[iRes + 1]) as ElementIndex[];
|
||||
if (isDefined(row.label_atom_id)) {
|
||||
filterInPlace(atomIdcs, iAtom => label_atom_id.value(iAtom) === row.label_atom_id);
|
||||
}
|
||||
if (isDefined(row.auth_atom_id)) {
|
||||
filterInPlace(atomIdcs, iAtom => auth_atom_id.value(iAtom) === row.auth_atom_id);
|
||||
}
|
||||
if (isDefined(row.type_symbol)) {
|
||||
filterInPlace(atomIdcs, iAtom => type_symbol.value(iAtom) === row.type_symbol?.toUpperCase());
|
||||
}
|
||||
arrayExtend(result, atomIdcs);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return index of atom in `model` which satistfies criteria given by `row`, if any.
|
||||
* Only works when `row.atom_id` and/or `row.atom_index` is defined (otherwise use `getAtomRangesForRow`). */
|
||||
function getTheAtomForRow(model: Model, row: MVSAnnotationRow, indices: IndicesAndSortings): ElementIndex | undefined {
|
||||
let iAtom: ElementIndex | undefined = undefined;
|
||||
if (!isDefined(row.atom_id) && !isDefined(row.atom_index)) throw new Error('ArgumentError: at least one of row.atom_id, row.atom_index must be defined.');
|
||||
if (isDefined(row.atom_id) && isDefined(row.atom_index)) {
|
||||
const a1 = indices.atomsById.get(row.atom_id);
|
||||
const a2 = indices.atomsByIndex.get(row.atom_index);
|
||||
if (a1 !== a2) return undefined;
|
||||
iAtom = a1;
|
||||
}
|
||||
if (isDefined(row.atom_id)) {
|
||||
iAtom = indices.atomsById.get(row.atom_id);
|
||||
}
|
||||
if (isDefined(row.atom_index)) {
|
||||
iAtom = indices.atomsByIndex.get(row.atom_index);
|
||||
}
|
||||
if (iAtom === undefined) return undefined;
|
||||
if (!atomQualifies(model, iAtom, row)) return undefined;
|
||||
return iAtom;
|
||||
}
|
||||
|
||||
/** Return true if `iAtom`-th atom in `model` satisfies all selection criteria given by `row`. */
|
||||
export function atomQualifies(model: Model, iAtom: ElementIndex, row: MVSAnnotationRow): boolean {
|
||||
const h = model.atomicHierarchy;
|
||||
|
||||
const iChain = h.chainAtomSegments.index[iAtom];
|
||||
const label_asym_id = h.chains.label_asym_id.value(iChain);
|
||||
const auth_asym_id = h.chains.auth_asym_id.value(iChain);
|
||||
const label_entity_id = h.chains.label_entity_id.value(iChain);
|
||||
if (!matches(row.label_asym_id, label_asym_id)) return false;
|
||||
if (!matches(row.auth_asym_id, auth_asym_id)) return false;
|
||||
if (!matches(row.label_entity_id, label_entity_id)) return false;
|
||||
|
||||
const iRes = h.residueAtomSegments.index[iAtom];
|
||||
const label_seq_id = (h.residues.label_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.label_seq_id.value(iRes) : undefined;
|
||||
const auth_seq_id = (h.residues.auth_seq_id.valueKind(iRes) === Column.ValueKind.Present) ? h.residues.auth_seq_id.value(iRes) : undefined;
|
||||
const pdbx_PDB_ins_code = h.residues.pdbx_PDB_ins_code.value(iRes);
|
||||
if (!matches(row.label_seq_id, label_seq_id)) return false;
|
||||
if (!matches(row.auth_seq_id, auth_seq_id)) return false;
|
||||
if (!matches(row.pdbx_PDB_ins_code, pdbx_PDB_ins_code)) return false;
|
||||
if (!matchesRange(row.beg_label_seq_id, row.end_label_seq_id, label_seq_id)) return false;
|
||||
if (!matchesRange(row.beg_auth_seq_id, row.end_auth_seq_id, auth_seq_id)) return false;
|
||||
|
||||
const label_atom_id = h.atoms.label_atom_id.value(iAtom);
|
||||
const auth_atom_id = h.atoms.auth_atom_id.value(iAtom);
|
||||
const type_symbol = h.atoms.type_symbol.value(iAtom);
|
||||
const atom_id = model.atomicConformation.atomId.value(iAtom);
|
||||
const atom_index = h.atomSourceIndex.value(iAtom);
|
||||
if (!matches(row.label_atom_id, label_atom_id)) return false;
|
||||
if (!matches(row.auth_atom_id, auth_atom_id)) return false;
|
||||
if (!matches(row.type_symbol?.toUpperCase(), type_symbol)) return false;
|
||||
if (!matches(row.atom_id, atom_id)) return false;
|
||||
if (!matches(row.atom_index, atom_index)) return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Return true if `value` equals `requiredValue` or if `requiredValue` if not defined. */
|
||||
function matches<T>(requiredValue: T | undefined | null, value: T | undefined): boolean {
|
||||
return !isDefined(requiredValue) || value === requiredValue;
|
||||
}
|
||||
|
||||
/** Return true if `requiredMin <= value <= requiredMax`.
|
||||
* Undefined `requiredMin` behaves like negative infinity.
|
||||
* Undefined `requiredMax` behaves like positive infinity. */
|
||||
function matchesRange<T>(requiredMin: T | undefined | null, requiredMax: T | undefined | null, value: T | undefined): boolean {
|
||||
if (isDefined(requiredMin) && (!isDefined(value) || value < requiredMin)) return false;
|
||||
if (isDefined(requiredMax) && (!isDefined(value) || value > requiredMax)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Convert an annotation row into a MolScript expression */
|
||||
export function rowToExpression(row: MVSAnnotationRow): Expression {
|
||||
const { and } = MS.core.logic;
|
||||
const { eq, gre: gte, lte } = MS.core.rel;
|
||||
const { macromolecular } = MS.struct.atomProperty;
|
||||
const propTests: Partial<Record<string, Expression>> = {};
|
||||
|
||||
if (isDefined(row.label_entity_id)) {
|
||||
propTests['entity-test'] = eq([macromolecular.label_entity_id(), row.label_entity_id]);
|
||||
}
|
||||
|
||||
const chainTests: Expression[] = [];
|
||||
if (isDefined(row.label_asym_id)) chainTests.push(eq([macromolecular.label_asym_id(), row.label_asym_id]));
|
||||
if (isDefined(row.auth_asym_id)) chainTests.push(eq([macromolecular.auth_asym_id(), row.auth_asym_id]));
|
||||
|
||||
if (chainTests.length === 1) {
|
||||
propTests['chain-test'] = chainTests[0];
|
||||
} else if (chainTests.length > 1) {
|
||||
propTests['chain-test'] = and(chainTests);
|
||||
}
|
||||
|
||||
const residueTests: Expression[] = [];
|
||||
if (isDefined(row.label_seq_id)) residueTests.push(eq([macromolecular.label_seq_id(), row.label_seq_id]));
|
||||
if (isDefined(row.auth_seq_id)) residueTests.push(eq([macromolecular.auth_seq_id(), row.auth_seq_id]));
|
||||
if (isDefined(row.pdbx_PDB_ins_code)) residueTests.push(eq([macromolecular.pdbx_PDB_ins_code(), row.pdbx_PDB_ins_code]));
|
||||
if (isDefined(row.beg_label_seq_id)) residueTests.push(gte([macromolecular.label_seq_id(), row.beg_label_seq_id]));
|
||||
if (isDefined(row.end_label_seq_id)) residueTests.push(lte([macromolecular.label_seq_id(), row.end_label_seq_id]));
|
||||
if (isDefined(row.beg_auth_seq_id)) residueTests.push(gte([macromolecular.auth_seq_id(), row.beg_auth_seq_id]));
|
||||
if (isDefined(row.end_auth_seq_id)) residueTests.push(lte([macromolecular.auth_seq_id(), row.end_auth_seq_id]));
|
||||
if (residueTests.length === 1) {
|
||||
propTests['residue-test'] = residueTests[0];
|
||||
} else if (residueTests.length > 1) {
|
||||
propTests['residue-test'] = and(residueTests);
|
||||
}
|
||||
|
||||
const atomTests: Expression[] = [];
|
||||
if (isDefined(row.atom_id)) atomTests.push(eq([macromolecular.id(), row.atom_id]));
|
||||
if (isDefined(row.atom_index)) atomTests.push(eq([MS.struct.atomProperty.core.sourceIndex(), row.atom_index]));
|
||||
if (isDefined(row.label_atom_id)) atomTests.push(eq([macromolecular.label_atom_id(), row.label_atom_id]));
|
||||
if (isDefined(row.auth_atom_id)) atomTests.push(eq([macromolecular.auth_atom_id(), row.auth_atom_id]));
|
||||
if (isDefined(row.type_symbol)) atomTests.push(eq([MS.struct.atomProperty.core.elementSymbol(), row.type_symbol.toUpperCase()]));
|
||||
if (atomTests.length === 1) {
|
||||
propTests['atom-test'] = atomTests[0];
|
||||
} else if (atomTests.length > 1) {
|
||||
propTests['atom-test'] = and(atomTests);
|
||||
}
|
||||
|
||||
return MS.struct.generator.atomGroups(propTests);
|
||||
}
|
||||
|
||||
/** Convert multiple annotation rows into a MolScript expression.
|
||||
* (with union semantics, i.e. an atom qualifies if it qualifies for at least one of the rows) */
|
||||
export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression {
|
||||
return unionExpression(rows.map(rowToExpression));
|
||||
}
|
||||
|
||||
/** Create MolScript expression covering the set union of the given expressions */
|
||||
function unionExpression(expressions: Expression[]): Expression {
|
||||
return MS.struct.combinator.merge(expressions.map(e => MS.struct.modifier.union([e])));
|
||||
}
|
||||
|
||||
|
||||
/** Data structure for an array divided into contiguous groups */
|
||||
interface GroupedArray<T> {
|
||||
/** Number of groups */
|
||||
count: number,
|
||||
/** Get size of i-th group as `offsets[i+1]-offsets[i]`.
|
||||
* Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
offsets: number[],
|
||||
/** Get j-th element in i-th group as `grouped[offsets[i]+j]` */
|
||||
grouped: T[],
|
||||
}
|
||||
|
||||
/** Return row indices grouped by `row.group_id`. Rows with `row.group_id===undefined` are treated as separate groups. */
|
||||
export function groupRows(rows: readonly MVSAnnotationRow[]): GroupedArray<number> {
|
||||
let counter = 0;
|
||||
const groupMap = new Map<string, number>();
|
||||
const groups: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const group_id = rows[i].group_id;
|
||||
if (group_id === undefined) {
|
||||
groups.push(counter++);
|
||||
} else {
|
||||
const groupIndex = groupMap.get(group_id);
|
||||
if (groupIndex === undefined) {
|
||||
groupMap.set(group_id, counter);
|
||||
groups.push(counter);
|
||||
counter++;
|
||||
} else {
|
||||
groups.push(groupIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
const rowIndices = range(rows.length).sort((i, j) => groups[i] - groups[j]);
|
||||
const offsets: number[] = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
if (i === 0 || groups[rowIndices[i]] !== groups[rowIndices[i - 1]]) offsets.push(i);
|
||||
}
|
||||
offsets.push(rowIndices.length);
|
||||
return { count: offsets.length - 1, offsets, grouped: rowIndices };
|
||||
}
|
||||
127
src/extensions/mvs/helpers/utils.ts
Normal file
127
src/extensions/mvs/helpers/utils.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { hashString } from '../../../mol-data/util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ColorNames } from '../../../mol-util/color/names';
|
||||
|
||||
|
||||
/** Represents either the result or the reason of failure of an operation that might have failed */
|
||||
export type Maybe<T> = { ok: true, value: T } | { ok: false, error: any }
|
||||
|
||||
/** Try to await a promise and return an object with its result (if resolved) or with the error (if rejected) */
|
||||
export async function safePromise<T>(promise: T): Promise<Maybe<Awaited<T>>> {
|
||||
try {
|
||||
const value = await promise;
|
||||
return { ok: true, value };
|
||||
} catch (error) {
|
||||
return { ok: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** A map where values are arrays. Handles missing keys when adding values. */
|
||||
export class MultiMap<K, V> implements Mapping<K, V[]> {
|
||||
private _map = new Map();
|
||||
|
||||
/** Return the array of values assidned to a key (or `undefined` if no such values) */
|
||||
get(key: K): V[] | undefined {
|
||||
return this._map.get(key);
|
||||
}
|
||||
/** Append value to a key (handles missing keys) */
|
||||
add(key: K, value: V) {
|
||||
if (!this._map.has(key)) {
|
||||
this._map.set(key, []);
|
||||
}
|
||||
this._map.get(key)!.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
/** Basic subset of `Map<K, V>`, only needs to have `get` method */
|
||||
export type Mapping<K, V> = Pick<Map<K, V>, 'get'>
|
||||
|
||||
/** Implementation of `Map` where keys are integers
|
||||
* and most keys are expected to be from interval `[0, limit)`.
|
||||
* For the keys within this interval, performance is better than `Map` (implemented by array).
|
||||
* For the keys out of this interval, performance is slightly worse than `Map`. */
|
||||
export class NumberMap<K extends number, V> implements Mapping<K, V> {
|
||||
private array: V[];
|
||||
private map: Map<K, V>;
|
||||
constructor(public readonly limit: K) {
|
||||
this.array = new Array(limit);
|
||||
this.map = new Map();
|
||||
}
|
||||
get(key: K): V | undefined {
|
||||
if (0 <= key && key < this.limit) return this.array[key];
|
||||
else return this.map.get(key);
|
||||
}
|
||||
set(key: K, value: V): void {
|
||||
if (0 <= key && key < this.limit) this.array[key] = value;
|
||||
else this.map.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Return `true` if `value` is not `undefined` or `null`.
|
||||
* Prefer this over `value !== undefined`
|
||||
* (for maybe if we want to allow `null` in `AnnotationRow` in the future) */
|
||||
export function isDefined<T>(value: T | undefined | null): value is T {
|
||||
return value !== undefined && value !== null;
|
||||
}
|
||||
/** Return `true` if at least one of `values` is not `undefined` or `null`. */
|
||||
export function isAnyDefined(...values: any[]): boolean {
|
||||
return values.some(v => isDefined(v));
|
||||
}
|
||||
/** Return filtered array containing all original elements except `undefined` or `null`. */
|
||||
export function filterDefined<T>(elements: (T | undefined | null)[]): T[] {
|
||||
return elements.filter(x => x !== undefined && x !== null) as T[];
|
||||
}
|
||||
|
||||
/** Create an 8-hex-character hash for a given input string, e.g. 'spanish inquisition' -> '7f9ac4be' */
|
||||
function stringHash32(input: string): string {
|
||||
const uint32hash = hashString(input) >>> 0; // >>>0 converts to uint32, LOL
|
||||
return uint32hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
/** Create an 16-hex-character hash for a given input string, e.g. 'spanish inquisition' -> '7f9ac4be544330be'*/
|
||||
export function stringHash(input: string): string {
|
||||
const reversed = input.split('').reverse().join('');
|
||||
return stringHash32(input) + stringHash32(reversed);
|
||||
}
|
||||
|
||||
/** Return type of elements in a set */
|
||||
export type ElementOfSet<S> = S extends Set<infer T> ? T : never
|
||||
|
||||
|
||||
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
|
||||
* Return `undefined` if `colorString` cannot be converted. */
|
||||
export function decodeColor(colorString: string | undefined): Color | undefined {
|
||||
if (colorString === undefined) return undefined;
|
||||
let result: Color | undefined;
|
||||
if (HexColor.is(colorString)) {
|
||||
if (colorString.length === 4) {
|
||||
// convert short form to full form (#f0f -> #ff00ff)
|
||||
colorString = `#${colorString[1]}${colorString[1]}${colorString[2]}${colorString[2]}${colorString[3]}${colorString[3]}`;
|
||||
}
|
||||
result = Color.fromHexStyle(colorString);
|
||||
if (result !== undefined && !isNaN(result)) return result;
|
||||
}
|
||||
result = ColorNames[colorString.toLowerCase() as keyof typeof ColorNames];
|
||||
if (result !== undefined) return result;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Regular expression matching a hexadecimal color string, e.g. '#FF1100' or '#f10' */
|
||||
const hexColorRegex = /^#([0-9A-F]{3}){1,2}$/i;
|
||||
|
||||
/** Hexadecimal color string, e.g. '#FF1100' (the type matches more than just valid HexColor strings) */
|
||||
export type HexColor = `#${string}`
|
||||
|
||||
export const HexColor = {
|
||||
/** Decide if a string is a valid hexadecimal color string (6-digit or 3-digit, e.g. '#FF1100' or '#f10') */
|
||||
is(str: any): str is HexColor {
|
||||
return typeof str === 'string' && hexColorRegex.test(str);
|
||||
},
|
||||
};
|
||||
490
src/extensions/mvs/load-helpers.ts
Normal file
490
src/extensions/mvs/load-helpers.ts
Normal file
@@ -0,0 +1,490 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { StructureComponentParams } from '../../mol-plugin-state/helpers/structure-component';
|
||||
import { StructureFromModel, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateBuilder, StateObject, StateObjectSelector, StateTransform, StateTransformer } from '../../mol-state';
|
||||
import { arrayDistinct } from '../../mol-util/array';
|
||||
import { canonicalJsonString } from '../../mol-util/json';
|
||||
import { stringToWords } from '../../mol-util/string';
|
||||
import { MVSAnnotationColorThemeProps, MVSAnnotationColorThemeProvider } from './components/annotation-color-theme';
|
||||
import { MVSAnnotationLabelRepresentationProvider } from './components/annotation-label/representation';
|
||||
import { MVSAnnotationSpec } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponentProps } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProps } from './components/annotation-tooltips-prop';
|
||||
import { CustomLabelTextProps } from './components/custom-label/visual';
|
||||
import { CustomTooltipsProps } from './components/custom-tooltips-prop';
|
||||
import { MultilayerColorThemeName, MultilayerColorThemeProps, NoColor } from './components/multilayer-color-theme';
|
||||
import { SelectorAll } from './components/selector';
|
||||
import { rowToExpression, rowsToExpression } from './helpers/selections';
|
||||
import { ElementOfSet, decodeColor, isDefined, stringHash } from './helpers/utils';
|
||||
import { MolstarLoadingContext } from './load';
|
||||
import { Kind, ParamsOfKind, SubTree, SubTreeOfKind, Tree, getChildren } from './tree/generic/tree-schema';
|
||||
import { dfs, formatObject } from './tree/generic/tree-utils';
|
||||
import { MolstarKind, MolstarNode, MolstarTree } from './tree/molstar/molstar-tree';
|
||||
import { DefaultColor } from './tree/mvs/mvs-defaults';
|
||||
|
||||
|
||||
/** Function responsible for loading a tree node `node` into Mol*.
|
||||
* Should apply changes within `updateParent.update` but not commit them.
|
||||
* Should modify `context` accordingly, if it is needed for loading other nodes later.
|
||||
* `updateParent.selector` is the result of loading the node's parent into Mol* state hierarchy (or the hierarchy root in case of root node). */
|
||||
export type LoadingAction<TNode extends Tree, TContext> = (updateParent: UpdateTarget, node: TNode, context: TContext) => UpdateTarget | undefined
|
||||
|
||||
/** Loading actions for loading a tree into Mol*, per node kind. */
|
||||
export type LoadingActions<TTree extends Tree, TContext> = { [kind in Kind<SubTree<TTree>>]?: LoadingAction<SubTreeOfKind<TTree, kind>, TContext> }
|
||||
|
||||
/** Load a tree into Mol*, by applying loading actions in DFS order and then commiting at once.
|
||||
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state. */
|
||||
export async function loadTree<TTree extends Tree, TContext>(plugin: PluginContext, tree: TTree, loadingActions: LoadingActions<TTree, TContext>, context: TContext, options?: { replaceExisting?: boolean }) {
|
||||
const mapping = new Map<SubTree<TTree>, UpdateTarget | undefined>();
|
||||
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
|
||||
if (options?.replaceExisting) {
|
||||
UpdateTarget.deleteChildren(updateRoot);
|
||||
}
|
||||
dfs<TTree>(tree, (node, parent) => {
|
||||
const kind: Kind<typeof node> = node.kind;
|
||||
const action = loadingActions[kind] as LoadingAction<typeof node, TContext> | undefined;
|
||||
if (action) {
|
||||
const updateParent = parent ? mapping.get(parent) : updateRoot;
|
||||
if (updateParent) {
|
||||
const msNode = action(updateParent, node, context);
|
||||
mapping.set(node, msNode);
|
||||
} else {
|
||||
console.warn(`No target found for this "${node.kind}" node`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
await UpdateTarget.commit(updateRoot);
|
||||
}
|
||||
|
||||
|
||||
/** A wrapper for updating Mol* state, while using deterministic transform refs.
|
||||
* ```
|
||||
* updateTarget = UpdateTarget.create(plugin); // like update = plugin.build();
|
||||
* UpdateTarget.apply(updateTarget, transformer, params); // like update.to(selector).apply(transformer, params);
|
||||
* await UpdateTarget.commit(updateTarget); // like await update.commit();
|
||||
* ```
|
||||
*/
|
||||
export interface UpdateTarget {
|
||||
readonly update: StateBuilder.Root,
|
||||
readonly selector: StateObjectSelector,
|
||||
readonly refManager: RefManager,
|
||||
}
|
||||
export const UpdateTarget = {
|
||||
/** Create a new update, with `selector` pointing to the root. */
|
||||
create(plugin: PluginContext, replaceExisting: boolean): UpdateTarget {
|
||||
const update = plugin.build();
|
||||
const msTarget = update.toRoot().selector;
|
||||
const refManager = new RefManager(plugin, replaceExisting);
|
||||
return { update, selector: msTarget, refManager };
|
||||
},
|
||||
/** Add a child node to `target.selector`, return a new `UpdateTarget` pointing to the new child. */
|
||||
apply<A extends StateObject, B extends StateObject, P extends {}>(target: UpdateTarget, transformer: StateTransformer<A, B, P>, params?: Partial<P>, options?: Partial<StateTransform.Options>): UpdateTarget {
|
||||
let refSuffix: string = transformer.id;
|
||||
if (transformer.id === StructureRepresentation3D.id) {
|
||||
const reprType = (params as any)?.type?.name ?? '';
|
||||
refSuffix += `:${reprType}`;
|
||||
}
|
||||
const ref = target.refManager.getChildRef(target.selector, refSuffix);
|
||||
const msResult = target.update.to(target.selector).apply(transformer, params, { ...options, ref }).selector;
|
||||
return { ...target, selector: msResult };
|
||||
},
|
||||
/** Delete all children of `target.selector`. */
|
||||
deleteChildren(target: UpdateTarget): UpdateTarget {
|
||||
const children = target.update.currentTree.children.get(target.selector.ref);
|
||||
children.forEach(child => target.update.delete(child));
|
||||
return target;
|
||||
},
|
||||
/** Commit all changes done in the current update. */
|
||||
commit(target: UpdateTarget): Promise<void> {
|
||||
return target.update.commit();
|
||||
},
|
||||
};
|
||||
|
||||
/** Manages transform refs in a deterministic way. Uses refs like !mvs:3ce3664304d32c5d:0 */
|
||||
class RefManager {
|
||||
/** For each hash (e.g. 3ce3664304d32c5d), store the number of already used refs with that hash. */
|
||||
private _counter: Record<string, number> = {};
|
||||
constructor(plugin: PluginContext, replaceExisting: boolean) {
|
||||
if (!replaceExisting) {
|
||||
plugin.state.data.cells.forEach(cell => {
|
||||
const ref = cell.transform.ref;
|
||||
if (ref.startsWith('!mvs:')) {
|
||||
const [_, hash, idNumber] = ref.split(':');
|
||||
const nextIdNumber = parseInt(idNumber) + 1;
|
||||
if (nextIdNumber > (this._counter[hash] ?? 0)) {
|
||||
this._counter[hash] = nextIdNumber;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
/** Return ref for a new node with given `hash`; update the counter accordingly. */
|
||||
private nextRef(hash: string): string {
|
||||
this._counter[hash] ??= 0;
|
||||
const idNumber = this._counter[hash]++;
|
||||
return `!mvs:${hash}:${idNumber}`;
|
||||
}
|
||||
/** Return ref for a new node based on parent and desired suffix. */
|
||||
getChildRef(parent: StateObjectSelector, suffix: string): string {
|
||||
const hashBase = parent.ref.replace(/^!mvs:/, '') + ':' + suffix;
|
||||
const hash = stringHash(hashBase);
|
||||
const result = this.nextRef(hash);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
|
||||
export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
|
||||
|
||||
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
|
||||
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
|
||||
|
||||
|
||||
/** Return a 4x4 matrix representing a rotation followed by a translation */
|
||||
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {
|
||||
if (rotation && rotation.length !== 9) throw new Error(`'rotation' param for 'transform' node must be array of 9 elements, found ${rotation}`);
|
||||
if (translation && translation.length !== 3) throw new Error(`'translation' param for 'transform' node must be array of 3 elements, found ${translation}`);
|
||||
const T = Mat4.identity();
|
||||
if (rotation) {
|
||||
const rotMatrix = Mat3.fromArray(Mat3(), rotation, 0);
|
||||
ensureRotationMatrix(rotMatrix, rotMatrix);
|
||||
Mat4.fromMat3(T, rotMatrix);
|
||||
}
|
||||
if (translation) {
|
||||
Mat4.setTranslation(T, Vec3.fromArray(Vec3(), translation, 0));
|
||||
}
|
||||
if (!Mat4.isRotationAndTranslation(T)) throw new Error(`'rotation' param for 'transform' is not a valid rotation matrix: ${rotation}`);
|
||||
return T;
|
||||
}
|
||||
|
||||
/** Adjust values in a close-to-rotation matrix `a` to ensure it is a proper rotation matrix
|
||||
* (i.e. its columns and rows are orthonormal and determinant equal to 1, within available precission). */
|
||||
function ensureRotationMatrix(out: Mat3, a: Mat3) {
|
||||
const x = Vec3.fromArray(_tmpVecX, a, 0);
|
||||
const y = Vec3.fromArray(_tmpVecY, a, 3);
|
||||
const z = Vec3.fromArray(_tmpVecZ, a, 6);
|
||||
Vec3.normalize(x, x);
|
||||
Vec3.orthogonalize(y, x, y);
|
||||
Vec3.normalize(z, Vec3.cross(z, x, y));
|
||||
Mat3.fromColumns(out, x, y, z);
|
||||
return out;
|
||||
}
|
||||
const _tmpVecX = Vec3();
|
||||
const _tmpVecY = Vec3();
|
||||
const _tmpVecZ = Vec3();
|
||||
|
||||
/** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */
|
||||
export function transformProps(node: SubTreeOfKind<MolstarTree, 'structure'>): StateTransformer.Params<TransformStructureConformation>[] {
|
||||
const result = [] as StateTransformer.Params<TransformStructureConformation>[];
|
||||
const transforms = getChildren(node).filter(c => c.kind === 'transform') as MolstarNode<'transform'>[];
|
||||
for (const transform of transforms) {
|
||||
const { rotation, translation } = transform.params;
|
||||
const matrix = transformFromRotationTranslation(rotation, translation);
|
||||
result.push({ transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Collect distinct annotation specs from all nodes in `tree` and set `context.annotationMap[node]` to respective annotationIds */
|
||||
export function collectAnnotationReferences(tree: SubTree<MolstarTree>, context: MolstarLoadingContext): MVSAnnotationSpec[] {
|
||||
const distinctSpecs: { [key: string]: MVSAnnotationSpec } = {};
|
||||
dfs(tree, node => {
|
||||
let spec: Omit<MVSAnnotationSpec, 'id'> | undefined = undefined;
|
||||
if (AnnotationFromUriKinds.has(node.kind as any)) {
|
||||
const p = (node as MolstarNode<AnnotationFromUriKind>).params;
|
||||
spec = { source: { name: 'url', params: { url: p.uri, format: p.format } }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name ?? undefined };
|
||||
} else if (AnnotationFromSourceKinds.has(node.kind as any)) {
|
||||
const p = (node as MolstarNode<AnnotationFromSourceKind>).params;
|
||||
spec = { source: { name: 'source-cif', params: {} }, schema: p.schema, cifBlock: blockSpec(p.block_header, p.block_index), cifCategory: p.category_name ?? undefined };
|
||||
}
|
||||
if (spec) {
|
||||
const key = canonicalJsonString(spec as any);
|
||||
distinctSpecs[key] ??= { ...spec, id: stringHash(key) };
|
||||
(context.annotationMap ??= new Map()).set(node, distinctSpecs[key].id);
|
||||
}
|
||||
});
|
||||
return Object.values(distinctSpecs);
|
||||
}
|
||||
function blockSpec(header: string | null | undefined, index: number | null | undefined): MVSAnnotationSpec['cifBlock'] {
|
||||
if (isDefined(header)) {
|
||||
return { name: 'header', params: { header: header } };
|
||||
} else {
|
||||
return { name: 'index', params: { index: index ?? 0 } };
|
||||
}
|
||||
}
|
||||
|
||||
/** Collect annotation tooltips from all nodes in `tree` and map them to annotationIds. */
|
||||
export function collectAnnotationTooltips(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): MVSAnnotationTooltipsProps['tooltips'] {
|
||||
const annotationTooltips: MVSAnnotationTooltipsProps['tooltips'] = [];
|
||||
dfs(tree, node => {
|
||||
if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') {
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
if (annotationId) {
|
||||
annotationTooltips.push({ annotationId, fieldName: node.params.field_name });
|
||||
};
|
||||
}
|
||||
});
|
||||
return arrayDistinct(annotationTooltips);
|
||||
}
|
||||
/** Collect inline tooltips from all nodes in `tree`. */
|
||||
export function collectInlineTooltips(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): CustomTooltipsProps['tooltips'] {
|
||||
const inlineTooltips: CustomTooltipsProps['tooltips'] = [];
|
||||
dfs(tree, (node, parent) => {
|
||||
if (node.kind === 'tooltip') {
|
||||
if (parent?.kind === 'component') {
|
||||
inlineTooltips.push({
|
||||
text: node.params.text,
|
||||
selector: componentPropsFromSelector(parent.params.selector),
|
||||
});
|
||||
} else if (parent?.kind === 'component_from_uri' || parent?.kind === 'component_from_source') {
|
||||
const p = componentFromXProps(parent, context);
|
||||
if (isDefined(p.annotationId) && isDefined(p.fieldName) && isDefined(p.fieldValues)) {
|
||||
inlineTooltips.push({
|
||||
text: node.params.text,
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return inlineTooltips;
|
||||
}
|
||||
/** Collect inline labels from all nodes in `tree`. */
|
||||
export function collectInlineLabels(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): CustomLabelTextProps['items'] {
|
||||
const inlineLabels: CustomLabelTextProps['items'] = [];
|
||||
dfs(tree, (node, parent) => {
|
||||
if (node.kind === 'label') {
|
||||
if (parent?.kind === 'component') {
|
||||
inlineLabels.push({
|
||||
text: node.params.text,
|
||||
position: {
|
||||
name: 'selection',
|
||||
params: {
|
||||
selector: componentPropsFromSelector(parent.params.selector),
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (parent?.kind === 'component_from_uri' || parent?.kind === 'component_from_source') {
|
||||
const p = componentFromXProps(parent, context);
|
||||
if (isDefined(p.annotationId) && isDefined(p.fieldName) && isDefined(p.fieldValues)) {
|
||||
inlineLabels.push({
|
||||
text: node.params.text,
|
||||
position: {
|
||||
name: 'selection',
|
||||
params: {
|
||||
selector: {
|
||||
name: 'annotation',
|
||||
params: { annotationId: p.annotationId, fieldName: p.fieldName, fieldValues: p.fieldValues },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return inlineLabels;
|
||||
}
|
||||
|
||||
/** Return `true` for components nodes which only serve for tooltip placement (not to be created in the MolStar object hierarchy) */
|
||||
export function isPhantomComponent(node: SubTreeOfKind<MolstarTree, 'component' | 'component_from_uri' | 'component_from_source'>) {
|
||||
return node.children && node.children.every(child => child.kind === 'tooltip' || child.kind === 'label');
|
||||
// These nodes could theoretically be removed when converting MVS to Molstar tree, but would get very tricky if we allow nested components
|
||||
}
|
||||
|
||||
/** Create props for `StructureFromModel` transformer from a structure node. */
|
||||
export function structureProps(node: MolstarNode<'structure'>): StateTransformer.Params<StructureFromModel> {
|
||||
const params = node.params;
|
||||
switch (params.type) {
|
||||
case 'model':
|
||||
return {
|
||||
type: {
|
||||
name: 'model',
|
||||
params: {}
|
||||
},
|
||||
};
|
||||
case 'assembly':
|
||||
return {
|
||||
type: {
|
||||
name: 'assembly',
|
||||
params: { id: params.assembly_id ?? undefined }
|
||||
},
|
||||
};
|
||||
case 'symmetry':
|
||||
return {
|
||||
type: {
|
||||
name: 'symmetry',
|
||||
params: { ijkMin: Vec3.ofArray(params.ijk_min), ijkMax: Vec3.ofArray(params.ijk_max) }
|
||||
},
|
||||
};
|
||||
case 'symmetry_mates':
|
||||
return {
|
||||
type: {
|
||||
name: 'symmetry-mates',
|
||||
params: { radius: params.radius }
|
||||
}
|
||||
};
|
||||
default:
|
||||
throw new Error(`NotImplementedError: Loading action for "structure" node, type "${params.type}"`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Create value for `type` prop for `StructureComponent` transformer based on a MVS selector. */
|
||||
export function componentPropsFromSelector(selector?: ParamsOfKind<MolstarTree, 'component'>['selector']): StructureComponentParams['type'] {
|
||||
if (selector === undefined) {
|
||||
return SelectorAll;
|
||||
} else if (typeof selector === 'string') {
|
||||
return { name: 'static', params: selector };
|
||||
} else if (Array.isArray(selector)) {
|
||||
return { name: 'expression', params: rowsToExpression(selector) };
|
||||
} else {
|
||||
return { name: 'expression', params: rowToExpression(selector) };
|
||||
}
|
||||
}
|
||||
|
||||
/** Return a pretty name for a value of selector param, e.g. "protein" -> 'Protein', {label_asym_id: "A"} -> 'Custom Selection: {label_asym_id: "A"}' */
|
||||
export function prettyNameFromSelector(selector?: ParamsOfKind<MolstarTree, 'component'>['selector']): string {
|
||||
if (selector === undefined) {
|
||||
return 'All';
|
||||
} else if (typeof selector === 'string') {
|
||||
return stringToWords(selector);
|
||||
} else if (Array.isArray(selector)) {
|
||||
return `Custom Selection: [${selector.map(formatObject).join(', ')}]`;
|
||||
} else {
|
||||
return `Custom Selection: ${formatObject(selector)}`;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
|
||||
export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from_source'>, context: MolstarLoadingContext): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
const fieldName = node.params.field_name;
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
return {
|
||||
type: { name: MVSAnnotationLabelRepresentationProvider.name, params: { annotationId, fieldName } },
|
||||
colorTheme: colorThemeForNode(nearestReprNode, context),
|
||||
};
|
||||
}
|
||||
|
||||
/** Create props for `AnnotationStructureComponent` transformer from a component_from_* node. */
|
||||
export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'component_from_source'>, context: MolstarLoadingContext): Partial<MVSAnnotationStructureComponentProps> {
|
||||
const annotationId = context.annotationMap?.get(node);
|
||||
const { field_name, field_values } = node.params;
|
||||
return {
|
||||
annotationId,
|
||||
fieldName: field_name,
|
||||
fieldValues: field_values ? { name: 'selected', params: field_values.map(v => ({ value: v })) } : { name: 'all', params: {} },
|
||||
nullIfEmpty: false,
|
||||
};
|
||||
}
|
||||
|
||||
/** Create props for `StructureRepresentation3D` transformer from a representation node. */
|
||||
export function representationProps(params: ParamsOfKind<MolstarTree, 'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
|
||||
switch (params.type) {
|
||||
case 'cartoon':
|
||||
return {
|
||||
type: { name: 'cartoon', params: {} },
|
||||
};
|
||||
case 'ball_and_stick':
|
||||
return {
|
||||
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5 } },
|
||||
};
|
||||
case 'surface':
|
||||
return {
|
||||
type: { name: 'molecular-surface', params: {} },
|
||||
sizeTheme: { name: 'physical', params: { scale: 1 } },
|
||||
};
|
||||
default:
|
||||
throw new Error('NotImplementedError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
|
||||
export function colorThemeForNode(node: SubTreeOfKind<MolstarTree, 'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] {
|
||||
if (node?.kind === 'representation') {
|
||||
const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source') as MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>[];
|
||||
if (children.length === 0) {
|
||||
return {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(DefaultColor) },
|
||||
};
|
||||
} else if (children.length === 1 && appliesColorToWholeRepr(children[0])) {
|
||||
return colorThemeForNode(children[0], context);
|
||||
} else {
|
||||
const layers: MultilayerColorThemeProps['layers'] = children.map(
|
||||
c => ({ theme: colorThemeForNode(c, context), selection: componentPropsFromSelector(c.kind === 'color' ? c.params.selector : undefined) })
|
||||
);
|
||||
return {
|
||||
name: MultilayerColorThemeName,
|
||||
params: { layers },
|
||||
};
|
||||
}
|
||||
}
|
||||
let annotationId: string | undefined = undefined;
|
||||
let fieldName: string | undefined = undefined;
|
||||
let color: string | undefined = undefined;
|
||||
switch (node?.kind) {
|
||||
case 'color_from_uri':
|
||||
case 'color_from_source':
|
||||
annotationId = context.annotationMap?.get(node);
|
||||
fieldName = node.params.field_name;
|
||||
break;
|
||||
case 'color':
|
||||
color = node.params.color;
|
||||
break;
|
||||
}
|
||||
if (annotationId) {
|
||||
return {
|
||||
name: MVSAnnotationColorThemeProvider.name,
|
||||
params: { annotationId, fieldName, background: NoColor } satisfies Partial<MVSAnnotationColorThemeProps>,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
name: 'uniform',
|
||||
params: { value: decodeColor(color) },
|
||||
};
|
||||
}
|
||||
}
|
||||
function appliesColorToWholeRepr(node: MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>): boolean {
|
||||
if (node.kind === 'color') {
|
||||
return !isDefined(node.params.selector) || node.params.selector === 'all';
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** Create a mapping of nearest representation nodes for each node in the tree
|
||||
* (to transfer coloring to label nodes smartly).
|
||||
* Only considers nodes within the same 'structure' subtree. */
|
||||
export function makeNearestReprMap(root: MolstarTree) {
|
||||
const map = new Map<MolstarNode, MolstarNode<'representation'>>();
|
||||
// Propagate up:
|
||||
dfs(root, undefined, (node, parent) => {
|
||||
if (node.kind === 'representation') {
|
||||
map.set(node, node);
|
||||
}
|
||||
if (node.kind !== 'structure' && map.has(node) && parent && !map.has(parent)) { // do not propagate above the lowest structure node
|
||||
map.set(parent, map.get(node)!);
|
||||
}
|
||||
});
|
||||
// Propagate down:
|
||||
dfs(root, (node, parent) => {
|
||||
if (!map.has(node) && parent && map.has(parent)) {
|
||||
map.set(node, map.get(parent)!);
|
||||
}
|
||||
});
|
||||
return map;
|
||||
}
|
||||
218
src/extensions/mvs/load.ts
Normal file
218
src/extensions/mvs/load.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
|
||||
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
|
||||
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectSelector } from '../../mol-state';
|
||||
import { MolViewSpec } from './behavior';
|
||||
import { setCamera, setCanvas, setFocus } from './camera';
|
||||
import { MVSAnnotationsProvider } from './components/annotation-prop';
|
||||
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
|
||||
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
|
||||
import { CustomLabelProps, CustomLabelRepresentationProvider } from './components/custom-label/representation';
|
||||
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
|
||||
import { AnnotationFromSourceKind, AnnotationFromUriKind, LoadingActions, UpdateTarget, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, loadTree, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
|
||||
import { MVSData } from './mvs-data';
|
||||
import { ParamsOfKind, SubTreeOfKind, validateTree } from './tree/generic/tree-schema';
|
||||
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
|
||||
import { MolstarNode, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
|
||||
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
/** Load a MolViewSpec (MVS) tree into the Mol* plugin.
|
||||
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state.
|
||||
* If `options.sanityChecks`, run some sanity checks and print potential issues to the console.
|
||||
* `options.sourceUrl` serves as the base for resolving relative URLs/URIs and may itself be relative to the window URL. */
|
||||
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, sanityChecks?: boolean, sourceUrl?: string } = {}) {
|
||||
try {
|
||||
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
|
||||
validateTree(MVSTreeSchema, data.root, 'MVS');
|
||||
if (options.sanityChecks) mvsSanityCheck(data.root);
|
||||
const molstarTree = convertMvsToMolstar(data.root, options.sourceUrl);
|
||||
// console.log(`Converted MolStar tree:\n${MVSData.toPrettyString({ root: molstarTree, metadata: { version: 'x', timestamp: 'x' } })}`)
|
||||
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
|
||||
await loadMolstarTree(plugin, molstarTree, options);
|
||||
} catch (err) {
|
||||
plugin.log.error(`${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Load a `MolstarTree` into the Mol* plugin.
|
||||
* If `replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state. */
|
||||
async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options?: { replaceExisting?: boolean }) {
|
||||
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
|
||||
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
|
||||
|
||||
const context: MolstarLoadingContext = {};
|
||||
|
||||
await loadTree(plugin, tree, MolstarLoadingActions, context, options);
|
||||
|
||||
setCanvas(plugin, context.canvas);
|
||||
if (context.focus?.kind === 'camera') {
|
||||
await setCamera(plugin, context.focus.params);
|
||||
} else if (context.focus?.kind === 'focus') {
|
||||
await setFocus(plugin, context.focus.focusTarget, context.focus.params);
|
||||
} else {
|
||||
await setFocus(plugin, undefined, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
/** Mutable context for loading a `MolstarTree`, available throughout the loading. */
|
||||
export interface MolstarLoadingContext {
|
||||
/** Maps `*_from_[uri|source]` nodes to annotationId they should reference */
|
||||
annotationMap?: Map<MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, string>,
|
||||
/** Maps each node (on 'structure' or lower level) to its nearest 'representation' node */
|
||||
nearestReprMap?: Map<MolstarNode, MolstarNode<'representation'>>,
|
||||
focus?: { kind: 'camera', params: ParamsOfKind<MolstarTree, 'camera'> } | { kind: 'focus', focusTarget: StateObjectSelector, params: ParamsOfKind<MolstarTree, 'focus'> },
|
||||
canvas?: ParamsOfKind<MolstarTree, 'canvas'>,
|
||||
}
|
||||
|
||||
|
||||
/** Loading actions for loading a `MolstarTree`, per node kind. */
|
||||
const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext> = {
|
||||
root(updateParent: UpdateTarget, node: MolstarNode<'root'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.nearestReprMap = makeNearestReprMap(node);
|
||||
return updateParent;
|
||||
},
|
||||
download(updateParent: UpdateTarget, node: MolstarNode<'download'>): UpdateTarget {
|
||||
return UpdateTarget.apply(updateParent, Download, {
|
||||
url: node.params.url,
|
||||
isBinary: node.params.is_binary,
|
||||
});
|
||||
},
|
||||
parse(updateParent: UpdateTarget, node: MolstarNode<'parse'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, ParseCif, {});
|
||||
} else if (format === 'pdb') {
|
||||
return updateParent;
|
||||
} else {
|
||||
console.error(`Unknown format in "parse" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
trajectory(updateParent: UpdateTarget, node: MolstarNode<'trajectory'>): UpdateTarget | undefined {
|
||||
const format = node.params.format;
|
||||
if (format === 'cif') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromMmCif, {
|
||||
blockHeader: node.params.block_header ?? '', // Must set to '' because just undefined would get overwritten by createDefaults
|
||||
blockIndex: node.params.block_index ?? undefined,
|
||||
});
|
||||
} else if (format === 'pdb') {
|
||||
return UpdateTarget.apply(updateParent, TrajectoryFromPDB, {});
|
||||
} else {
|
||||
console.error(`Unknown format in "trajectory" node: "${format}"`);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
model(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'model'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const annotations = collectAnnotationReferences(node, context);
|
||||
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
|
||||
modelIndex: node.params.model_index,
|
||||
});
|
||||
UpdateTarget.apply(model, CustomModelProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationsProvider.descriptor.name]: { annotations }
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationsProvider.descriptor.name
|
||||
],
|
||||
});
|
||||
return model;
|
||||
},
|
||||
structure(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const props = structureProps(node);
|
||||
const struct = UpdateTarget.apply(updateParent, StructureFromModel, props);
|
||||
let transformed = struct;
|
||||
for (const t of transformProps(node)) {
|
||||
transformed = UpdateTarget.apply(transformed, TransformStructureConformation, t); // applying to the result of previous transform, to get the correct transform order
|
||||
}
|
||||
const annotationTooltips = collectAnnotationTooltips(node, context);
|
||||
const inlineTooltips = collectInlineTooltips(node, context);
|
||||
if (annotationTooltips.length + inlineTooltips.length > 0) {
|
||||
UpdateTarget.apply(struct, CustomStructureProperties, {
|
||||
properties: {
|
||||
[MVSAnnotationTooltipsProvider.descriptor.name]: { tooltips: annotationTooltips },
|
||||
[CustomTooltipsProvider.descriptor.name]: { tooltips: inlineTooltips },
|
||||
},
|
||||
autoAttach: [
|
||||
MVSAnnotationTooltipsProvider.descriptor.name,
|
||||
CustomTooltipsProvider.descriptor.name,
|
||||
],
|
||||
});
|
||||
}
|
||||
const inlineLabels = collectInlineLabels(node, context);
|
||||
if (inlineLabels.length > 0) {
|
||||
const nearestReprNode = context.nearestReprMap?.get(node);
|
||||
UpdateTarget.apply(struct, StructureRepresentation3D, {
|
||||
type: {
|
||||
name: CustomLabelRepresentationProvider.name,
|
||||
params: { items: inlineLabels } satisfies Partial<CustomLabelProps>,
|
||||
},
|
||||
colorTheme: colorThemeForNode(nearestReprNode, context),
|
||||
});
|
||||
}
|
||||
return struct;
|
||||
},
|
||||
tooltip: undefined, // No action needed, already loaded in `structure`
|
||||
tooltip_from_uri: undefined, // No action needed, already loaded in `structure`
|
||||
tooltip_from_source: undefined, // No action needed, already loaded in `structure`
|
||||
component(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component'>): UpdateTarget | undefined {
|
||||
if (isPhantomComponent(node)) {
|
||||
return updateParent;
|
||||
}
|
||||
const selector = node.params.selector;
|
||||
return UpdateTarget.apply(updateParent, StructureComponent, {
|
||||
type: componentPropsFromSelector(selector),
|
||||
label: prettyNameFromSelector(selector),
|
||||
nullIfEmpty: false,
|
||||
});
|
||||
},
|
||||
component_from_uri(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component_from_uri'>, context: MolstarLoadingContext): UpdateTarget | undefined {
|
||||
if (isPhantomComponent(node)) return undefined;
|
||||
const props = componentFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
|
||||
},
|
||||
component_from_source(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component_from_source'>, context: MolstarLoadingContext): UpdateTarget | undefined {
|
||||
if (isPhantomComponent(node)) return undefined;
|
||||
const props = componentFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
|
||||
},
|
||||
representation(updateParent: UpdateTarget, node: MolstarNode<'representation'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
return UpdateTarget.apply(updateParent, StructureRepresentation3D, {
|
||||
...representationProps(node.params),
|
||||
colorTheme: colorThemeForNode(node, context),
|
||||
});
|
||||
},
|
||||
color: undefined, // No action needed, already loaded in `representation`
|
||||
color_from_uri: undefined, // No action needed, already loaded in `representation`
|
||||
color_from_source: undefined, // No action needed, already loaded in `representation`
|
||||
label: undefined, // No action needed, already loaded in `structure`
|
||||
label_from_uri(updateParent: UpdateTarget, node: MolstarNode<'label_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const props = labelFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
|
||||
},
|
||||
label_from_source(updateParent: UpdateTarget, node: MolstarNode<'label_from_source'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
const props = labelFromXProps(node, context);
|
||||
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
|
||||
},
|
||||
focus(updateParent: UpdateTarget, node: MolstarNode<'focus'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.focus = { kind: 'focus', focusTarget: updateParent.selector, params: node.params };
|
||||
return updateParent;
|
||||
},
|
||||
camera(updateParent: UpdateTarget, node: MolstarNode<'camera'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.focus = { kind: 'camera', params: node.params };
|
||||
return updateParent;
|
||||
},
|
||||
canvas(updateParent: UpdateTarget, node: MolstarNode<'canvas'>, context: MolstarLoadingContext): UpdateTarget {
|
||||
context.canvas = node.params;
|
||||
return updateParent;
|
||||
},
|
||||
};
|
||||
98
src/extensions/mvs/mvs-data.ts
Normal file
98
src/extensions/mvs/mvs-data.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from './tree/generic/tree-schema';
|
||||
import { treeToString } from './tree/generic/tree-utils';
|
||||
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
|
||||
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
|
||||
|
||||
|
||||
/** Top level of the MolViewSpec (MVS) data format. */
|
||||
export interface MVSData {
|
||||
/** MolViewSpec tree */
|
||||
root: MVSTree,
|
||||
/** Associated metadata */
|
||||
metadata: MVSMetadata,
|
||||
}
|
||||
|
||||
interface MVSMetadata {
|
||||
/** Version of the spec used to write this tree */
|
||||
version: string,
|
||||
/** Name of this view */
|
||||
title?: string,
|
||||
/** Detailed description of this view */
|
||||
description?: string,
|
||||
/** Format of the description */
|
||||
description_format?: 'markdown' | 'plaintext',
|
||||
/** Timestamp when this view was exported */
|
||||
timestamp: string,
|
||||
}
|
||||
|
||||
export const MVSData = {
|
||||
/** Currently supported major version of MolViewSpec format (e.g. 1 for version '1.0.8') */
|
||||
SupportedVersion: 1,
|
||||
|
||||
/** Parse MVSJ (MolViewSpec-JSON) format to `MVSData`. Does not include any validation. */
|
||||
fromMVSJ(mvsjString: string): MVSData {
|
||||
const result: MVSData = JSON.parse(mvsjString);
|
||||
const major = majorVersion(result?.metadata?.version);
|
||||
if (major === undefined) {
|
||||
console.error('Loaded MVS does not contain valid version info.');
|
||||
} else if (major > (majorVersion(MVSData.SupportedVersion) ?? 0)) {
|
||||
console.warn(`Loaded MVS is of higher version (${result.metadata.version}) than currently supported version (${MVSData.SupportedVersion}). Some features may not work as expected.`);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Encode `MVSData` to MVSJ (MolViewSpec-JSON) string. Use `space` parameter to control formatting (as with `JSON.stringify`). */
|
||||
toMVSJ(mvsData: MVSData, space?: string | number): string {
|
||||
return JSON.stringify(mvsData, undefined, space);
|
||||
},
|
||||
|
||||
/** Validate `MVSData`. Return `true` if OK; `false` if not OK.
|
||||
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
|
||||
isValid(mvsData: MVSData, options: { noExtra?: boolean } = {}): boolean {
|
||||
return MVSData.validationIssues(mvsData, options) === undefined;
|
||||
},
|
||||
|
||||
/** Validate `MVSData`. Return `undefined` if OK; list of issues if not OK.
|
||||
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
|
||||
validationIssues(mvsData: MVSData, options: { noExtra?: boolean } = {}): string[] | undefined {
|
||||
const version = mvsData?.metadata?.version;
|
||||
if (typeof version !== 'string') return [`"version" in MVS must be a string, not ${typeof version}: ${version}`];
|
||||
if (mvsData.root === undefined) return [`"root" missing in MVS`];
|
||||
return treeValidationIssues(MVSTreeSchema, mvsData.root, options);
|
||||
},
|
||||
|
||||
/** Return a human-friendly textual representation of `mvsData`. */
|
||||
toPrettyString(mvsData: MVSData): string {
|
||||
const title = mvsData.metadata.title !== undefined ? ` "${mvsData.metadata.title}"` : '';
|
||||
return `MolViewSpec tree${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${treeToString(mvsData.root)}`;
|
||||
},
|
||||
|
||||
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
|
||||
*
|
||||
* ```
|
||||
* const builder = MVSData.createBuilder();
|
||||
* builder.canvas({ background_color: 'white' });
|
||||
* const struct = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure();
|
||||
* struct.component().representation().color({ color: '#3050F8' });
|
||||
* console.log(MVSData.toPrettyString(builder.getState()));
|
||||
* ```
|
||||
*/
|
||||
createBuilder(): Root {
|
||||
return createMVSBuilder();
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
/** Get the major version from a semantic version string, e.g. '1.0.8' -> 1 */
|
||||
function majorVersion(semanticVersion: string | number): number | undefined {
|
||||
if (typeof semanticVersion === 'string') return parseInt(semanticVersion.split('.')[0]);
|
||||
if (typeof semanticVersion === 'number') return Math.floor(semanticVersion);
|
||||
console.error(`Version should be a string, not ${typeof semanticVersion}: ${semanticVersion}`);
|
||||
return undefined;
|
||||
}
|
||||
107
src/extensions/mvs/tree/generic/_spec/params-schema.spec.ts
Normal file
107
src/extensions/mvs/tree/generic/_spec/params-schema.spec.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
|
||||
import { fieldValidationIssues, RequiredField, literal, nullable, paramsValidationIssues, OptionalField } from '../params-schema';
|
||||
|
||||
|
||||
describe('fieldValidationIssues', () => {
|
||||
it('fieldValidationIssues string', async () => {
|
||||
const stringField = RequiredField(iots.string);
|
||||
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues string choice', async () => {
|
||||
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'));
|
||||
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
|
||||
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues number choice', async () => {
|
||||
const numberParam = RequiredField(literal(1, 2, 3, 4));
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues int', async () => {
|
||||
const numberParam = RequiredField(iots.Integer);
|
||||
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
|
||||
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues union', async () => {
|
||||
const stringOrNumberParam = RequiredField(iots.union([iots.string, iots.number]));
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
|
||||
});
|
||||
it('fieldValidationIssues nullable', async () => {
|
||||
const stringOrNullParam = RequiredField(nullable(iots.string));
|
||||
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
|
||||
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
|
||||
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
const schema = {
|
||||
name: OptionalField(iots.string),
|
||||
surname: RequiredField(iots.string),
|
||||
lunch: RequiredField(iots.boolean),
|
||||
age: OptionalField(iots.number),
|
||||
};
|
||||
|
||||
describe('validateParams', () => {
|
||||
it('validateParams', async () => {
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, {}, { noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('validateFullParams', () => {
|
||||
it('validateFullParams', async () => {
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
|
||||
expect(paramsValidationIssues(schema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
|
||||
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
|
||||
});
|
||||
});
|
||||
|
||||
140
src/extensions/mvs/tree/generic/params-schema.ts
Normal file
140
src/extensions/mvs/tree/generic/params-schema.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { PathReporter } from 'io-ts/PathReporter';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
|
||||
|
||||
/** All types that can be used in tree node params.
|
||||
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
|
||||
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {}
|
||||
|
||||
/** Type definition for a string */
|
||||
export const str = iots.string;
|
||||
/** Type definition for an integer */
|
||||
export const int = iots.Integer;
|
||||
/** Type definition for a float or integer number */
|
||||
export const float = iots.number;
|
||||
/** Type definition for a boolean */
|
||||
export const bool = iots.boolean;
|
||||
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
|
||||
export const tuple = iots.tuple;
|
||||
/** Type definition for a list/array, e.g. `list(str)` */
|
||||
export const list = iots.array;
|
||||
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
|
||||
export const union = iots.union;
|
||||
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
|
||||
export function nullable<T extends iots.Type<any>>(type: T) {
|
||||
return union([type, iots.null]);
|
||||
}
|
||||
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
|
||||
export function literal<V extends string | number | boolean>(...values: V[]) {
|
||||
if (values.length === 0) {
|
||||
throw new Error(`literal type must have at least one value`);
|
||||
}
|
||||
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
|
||||
return new iots.Type<V>(
|
||||
typeName,
|
||||
((value: any) => values.includes(value)) as any,
|
||||
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
|
||||
value => value
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
|
||||
interface Field<V extends AllowedValueTypes = any, R extends boolean = boolean> {
|
||||
/** Definition of allowed types for the field */
|
||||
type: iots.Type<V>,
|
||||
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
|
||||
* If `required===false`, the value can be ommitted (meaning that a default should be used).
|
||||
* If `type` allows `null`, the default must be `null`. */
|
||||
required: R,
|
||||
/** Description of what the field value means */
|
||||
description?: string,
|
||||
}
|
||||
/** Schema for param field which must always be provided (has no default value) */
|
||||
export interface RequiredField<V extends AllowedValueTypes = any> extends Field<V> {
|
||||
required: true,
|
||||
}
|
||||
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): RequiredField<V> {
|
||||
return { type, required: true, description };
|
||||
}
|
||||
|
||||
/** Schema for param field which can be dropped (meaning that a default value will be used) */
|
||||
export interface OptionalField<V extends AllowedValueTypes = any> extends Field<V> {
|
||||
required: false,
|
||||
}
|
||||
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): OptionalField<V> {
|
||||
return { type, required: false, description };
|
||||
}
|
||||
|
||||
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
|
||||
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never
|
||||
|
||||
/** Type of valid default value for field of type `F` (if the field's type allows `null`, the default must be `null`) */
|
||||
export type DefaultFor<F extends Field> = F extends Field<infer V> ? (null extends V ? null : V) : never
|
||||
|
||||
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
|
||||
* Return description of validation issues, if `value` has wrong type. */
|
||||
export function fieldValidationIssues<F extends Field, V>(field: F, value: V): V extends ValueFor<F> ? undefined : string[] {
|
||||
const validation = field.type.decode(value);
|
||||
if (validation._tag === 'Right') {
|
||||
return undefined as any;
|
||||
} else {
|
||||
return PathReporter.report(validation) as any;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Schema for "params", i.e. a flat collection of key-value pairs */
|
||||
export type ParamsSchema<TKey extends string = string> = { [key in TKey]: Field }
|
||||
|
||||
/** Variation of a params schema where all fields are required */
|
||||
export type AllRequired<TParamsSchema extends ParamsSchema> = { [key in keyof TParamsSchema]: TParamsSchema[key] extends Field<infer V> ? RequiredField<V> : never }
|
||||
export function AllRequired<TParamsSchema extends ParamsSchema>(paramsSchema: TParamsSchema): AllRequired<TParamsSchema> {
|
||||
return mapObjectMap(paramsSchema, field => RequiredField(field.type, field.description)) as AllRequired<TParamsSchema>;
|
||||
}
|
||||
|
||||
/** Type of values for a params schema (optional fields can be missing) */
|
||||
export type ValuesFor<P extends ParamsSchema> =
|
||||
{ [key in keyof P as (P[key] extends RequiredField<any> ? key : never)]: ValueFor<P[key]> }
|
||||
& { [key in keyof P as (P[key] extends OptionalField<any> ? key : never)]?: ValueFor<P[key]> }
|
||||
|
||||
/** Type of full values for a params schema, i.e. including all optional fields */
|
||||
export type FullValuesFor<P extends ParamsSchema> = { [key in keyof P]: ValueFor<P[key]> }
|
||||
|
||||
/** Type of default values for a params schema, i.e. including only optional fields */
|
||||
export type DefaultsFor<P extends ParamsSchema> = { [key in keyof P as (P[key] extends Field<any, false> ? key : never)]: ValueFor<P[key]> }
|
||||
|
||||
|
||||
/** Return `undefined` if `values` contains correct value types for `schema`,
|
||||
* return description of validation issues, if `values` have wrong type.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
*/
|
||||
export function paramsValidationIssues<P extends ParamsSchema, V extends { [k: string]: any }>(schema: P, values: V, options: { requireAll?: boolean, noExtra?: boolean } = {}): string[] | undefined {
|
||||
if (!isPlainObject(values)) return [`Parameters must be an object, not ${values}`];
|
||||
for (const key in schema) {
|
||||
const paramDef = schema[key];
|
||||
if (Object.hasOwn(values, key)) {
|
||||
const value = values[key];
|
||||
const issues = fieldValidationIssues(paramDef, value);
|
||||
if (issues) return [`Invalid type for parameter "${key}":`, ...issues.map(s => ' ' + s)];
|
||||
} else {
|
||||
if (paramDef.required) return [`Missing required parameter "${key}".`];
|
||||
if (options.requireAll) return [`Missing optional parameter "${key}".`];
|
||||
}
|
||||
}
|
||||
if (options.noExtra) {
|
||||
for (const key in values) {
|
||||
if (!Object.hasOwn(schema, key)) return [`Unknown parameter "${key}".`];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
194
src/extensions/mvs/tree/generic/tree-schema.ts
Normal file
194
src/extensions/mvs/tree/generic/tree-schema.ts
Normal file
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { onelinerJsonString } from '../../../../mol-util/json';
|
||||
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
|
||||
import { AllRequired, DefaultsFor, ParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
|
||||
import { treeToString } from './tree-utils';
|
||||
|
||||
|
||||
/** Tree node without children */
|
||||
export type Node<TKind extends string = string, TParams extends {} = {}> =
|
||||
{} extends TParams ? {
|
||||
kind: TKind,
|
||||
params?: TParams,
|
||||
} : {
|
||||
kind: TKind,
|
||||
params: TParams,
|
||||
} // params can be dropped if {} is valid value for params
|
||||
|
||||
/** Kind type for a tree node */
|
||||
export type Kind<TNode extends Node> = TNode['kind']
|
||||
|
||||
/** Params type for a tree node */
|
||||
export type Params<TNode extends Node> = NonNullable<TNode['params']>
|
||||
|
||||
|
||||
/** Tree (i.e. a node with optional children) where the root node is of type `TRoot` and other nodes are of type `TNode` */
|
||||
export type Tree<TNode extends Node<string, {}> = Node<string, {}>, TRoot extends TNode = TNode> =
|
||||
TRoot & {
|
||||
children?: Tree<TNode, TNode>[],
|
||||
}
|
||||
|
||||
/** Type of any subtree that can occur within given `TTree` tree type */
|
||||
export type SubTree<TTree extends Tree> = NonNullable<TTree['children']>[number]
|
||||
|
||||
/** Type of any subtree that can occur within given `TTree` tree type and has kind type `TKind` */
|
||||
export type SubTreeOfKind<TTree extends Tree, TKind extends Kind<SubTree<TTree>> = Kind<SubTree<TTree>>> = RootOfKind<SubTree<TTree>, TKind>
|
||||
|
||||
type RootOfKind<TTree extends Tree, TKind extends Kind<TTree>> = Extract<TTree, Tree<any, Node<TKind>>>
|
||||
|
||||
/** Params type for a given kind type within a tree */
|
||||
export type ParamsOfKind<TTree extends Tree, TKind extends Kind<SubTree<TTree>> = Kind<SubTree<TTree>>> = NonNullable<SubTreeOfKind<TTree, TKind>['params']>
|
||||
|
||||
|
||||
/** Get params from a tree node */
|
||||
export function getParams<TNode extends Node>(node: TNode): Params<TNode> {
|
||||
return node.params ?? {};
|
||||
}
|
||||
/** Get children from a tree node */
|
||||
export function getChildren<TTree extends Tree>(tree: TTree): SubTree<TTree>[] {
|
||||
return tree.children ?? [];
|
||||
}
|
||||
|
||||
|
||||
type ParamsSchemas = { [kind: string]: ParamsSchema }
|
||||
|
||||
/** Definition of tree type, specifying allowed node kinds, types of their params, required kind for the root, and allowed parent-child kind combinations */
|
||||
export interface TreeSchema<TParamsSchemas extends ParamsSchemas = ParamsSchemas, TRootKind extends keyof TParamsSchemas = string> {
|
||||
/** Required kind of the root node */
|
||||
rootKind: TRootKind,
|
||||
/** Definition of allowed node kinds */
|
||||
nodes: {
|
||||
[kind in keyof TParamsSchemas]: {
|
||||
/** Params schema for this node kind */
|
||||
params: TParamsSchemas[kind],
|
||||
/** Documentation for this node kind */
|
||||
description?: string,
|
||||
/** Node kinds that can serve as parent for this node kind (`undefined` means the parent can be of any kind) */
|
||||
parent?: (string & keyof TParamsSchemas)[],
|
||||
}
|
||||
},
|
||||
}
|
||||
export function TreeSchema<P extends ParamsSchemas = ParamsSchemas, R extends keyof P = string>(schema: TreeSchema<P, R>): TreeSchema<P, R> {
|
||||
return schema as any;
|
||||
}
|
||||
|
||||
/** ParamsSchemas per node kind */
|
||||
type ParamsSchemasOf<TTreeSchema extends TreeSchema> = TTreeSchema extends TreeSchema<infer TParamsSchema, any> ? TParamsSchema : never;
|
||||
|
||||
/** Variation of params schemas where all param fields are required */
|
||||
type ParamsSchemasWithAllRequired<TParamsSchemas extends ParamsSchemas> = { [kind in keyof TParamsSchemas]: AllRequired<TParamsSchemas[kind]> }
|
||||
|
||||
/** Variation of a tree schema where all param fields are required */
|
||||
export type TreeSchemaWithAllRequired<TTreeSchema extends TreeSchema> = TreeSchema<ParamsSchemasWithAllRequired<ParamsSchemasOf<TTreeSchema>>, TTreeSchema['rootKind']>
|
||||
export function TreeSchemaWithAllRequired<TTreeSchema extends TreeSchema>(schema: TTreeSchema): TreeSchemaWithAllRequired<TTreeSchema> {
|
||||
return {
|
||||
...schema,
|
||||
nodes: mapObjectMap(schema.nodes, node => ({ ...node, params: AllRequired(node.params) })) as any,
|
||||
};
|
||||
}
|
||||
|
||||
/** Type of tree node which can occur as the root of a tree conforming to tree schema `TTreeSchema` */
|
||||
export type RootFor<TTreeSchema extends TreeSchema> = NodeFor<TTreeSchema, TTreeSchema['rootKind']>
|
||||
|
||||
/** Type of tree node which can occur anywhere in a tree conforming to tree schema `TTreeSchema`,
|
||||
* optionally narrowing down to a given node kind */
|
||||
export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSchemasOf<TTreeSchema> = keyof ParamsSchemasOf<TTreeSchema>>
|
||||
= { [key in keyof ParamsSchemasOf<TTreeSchema>]: Node<key & string, ValuesFor<ParamsSchemasOf<TTreeSchema>[key]>> }[TKind]
|
||||
|
||||
/** Type of tree which conforms to tree schema `TTreeSchema` */
|
||||
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
|
||||
|
||||
/** Type of default parameter values for each node kind in a tree schema `TTreeSchema` */
|
||||
export type DefaultsForTree<TTreeSchema extends TreeSchema> = { [kind in keyof TTreeSchema['nodes']]: DefaultsFor<TTreeSchema['nodes'][kind]['params']> }
|
||||
|
||||
|
||||
/** Return `undefined` if a tree conforms to the given schema,
|
||||
* return validation issues (as a list of lines) if it does not conform.
|
||||
* If `options.requireAll`, all parameters (including optional) must have a value provided.
|
||||
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
|
||||
* If `options.anyRoot` is true, the kind of the root node is not enforced.
|
||||
*/
|
||||
export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: { requireAll?: boolean, noExtra?: boolean, anyRoot?: boolean, parent?: string } = {}): string[] | undefined {
|
||||
if (!isPlainObject(tree)) return [`Node must be an object, not ${tree}`];
|
||||
if (!options.anyRoot && tree.kind !== schema.rootKind) return [`Invalid root node kind "${tree.kind}", root must be of kind "${schema.rootKind}"`];
|
||||
const nodeSchema = schema.nodes[tree.kind];
|
||||
if (!nodeSchema) return [`Unknown node kind "${tree.kind}"`];
|
||||
if (nodeSchema.parent && (options.parent !== undefined) && !nodeSchema.parent.includes(options.parent)) {
|
||||
return [`Node of kind "${tree.kind}" cannot appear as a child of "${options.parent}". Allowed parents for "${tree.kind}" are: ${nodeSchema.parent.map(s => `"${s}"`).join(', ')}`];
|
||||
}
|
||||
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
|
||||
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
|
||||
for (const child of getChildren(tree)) {
|
||||
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
|
||||
if (issues) return issues;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/** Validate a tree against the given schema.
|
||||
* Do nothing if OK; print validation issues on console and throw an error is the tree does not conform.
|
||||
* Include `label` in the printed output. */
|
||||
export function validateTree(schema: TreeSchema, tree: Tree, label: string): void {
|
||||
const issues = treeValidationIssues(schema, tree, { noExtra: true });
|
||||
if (issues) {
|
||||
console.warn(`Invalid ${label} tree:\n${treeToString(tree)}`);
|
||||
console.error(`${label} tree validation issues:`);
|
||||
for (const line of issues) {
|
||||
console.error(' ', line);
|
||||
}
|
||||
throw new Error('FormatError');
|
||||
}
|
||||
}
|
||||
|
||||
/** Return documentation for a tree schema as plain text */
|
||||
export function treeSchemaToString<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
|
||||
return treeSchemaToString_(schema, defaults, false);
|
||||
}
|
||||
/** Return documentation for a tree schema as markdown text */
|
||||
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
|
||||
return treeSchemaToString_(schema, defaults, true);
|
||||
}
|
||||
function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>, markdown: boolean = false): string {
|
||||
const out: string[] = [];
|
||||
const bold = (str: string) => markdown ? `**${str}**` : str;
|
||||
const code = (str: string) => markdown ? `\`${str}\`` : str;
|
||||
const h1 = markdown ? '## ' : ' - ';
|
||||
const p1 = markdown ? '' : ' ';
|
||||
const h2 = markdown ? '- ' : ' - ';
|
||||
const p2 = markdown ? ' ' : ' ';
|
||||
const newline = markdown ? '\n\n' : '\n';
|
||||
out.push(`Tree schema:`);
|
||||
for (const kind in schema.nodes) {
|
||||
const { description, params, parent } = schema.nodes[kind];
|
||||
out.push(`${h1}${code(kind)}`);
|
||||
if (kind === schema.rootKind) {
|
||||
out.push(`${p1}[Root of the tree must be of this kind]`);
|
||||
}
|
||||
if (description) {
|
||||
out.push(`${p1}${description}`);
|
||||
}
|
||||
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
|
||||
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
|
||||
for (const key in params) {
|
||||
const field = params[key];
|
||||
let typeString = field.type.name;
|
||||
if (typeString.startsWith('(') && typeString.endsWith(')')) {
|
||||
typeString = typeString.slice(1, -1);
|
||||
}
|
||||
out.push(`${h2}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(typeString)}`);
|
||||
const defaultValue = (defaults?.[kind] as any)?.[key];
|
||||
if (field.description) {
|
||||
out.push(`${p2}${field.description}`);
|
||||
}
|
||||
if (defaultValue !== undefined) {
|
||||
out.push(`${p2}Default: ${code(onelinerJsonString(defaultValue))}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.join(newline);
|
||||
}
|
||||
175
src/extensions/mvs/tree/generic/tree-utils.ts
Normal file
175
src/extensions/mvs/tree/generic/tree-utils.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { canonicalJsonString } from '../../../../mol-util/json';
|
||||
import { DefaultsForTree, Kind, SubTree, SubTreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
|
||||
|
||||
|
||||
/** Run DFS (depth-first search) algorithm on a rooted tree.
|
||||
* Runs `visit` function when a node is discovered (before visiting any descendants).
|
||||
* Runs `postVisit` function when leaving a node (after all descendants have been visited). */
|
||||
export function dfs<TTree extends Tree>(root: TTree, visit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any, postVisit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any) {
|
||||
return _dfs<SubTree<TTree>>(root, undefined, visit, postVisit);
|
||||
}
|
||||
function _dfs<TTree extends Tree>(root: TTree, parent: SubTree<TTree> | undefined, visit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any, postVisit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any) {
|
||||
if (visit) visit(root, parent);
|
||||
for (const child of root.children ?? []) {
|
||||
_dfs<SubTree<TTree>>(child, root, visit, postVisit);
|
||||
}
|
||||
if (postVisit) postVisit(root, parent);
|
||||
}
|
||||
|
||||
/** Convert a tree into a pretty-printed string. */
|
||||
export function treeToString(tree: Tree) {
|
||||
let level = 0;
|
||||
const lines: string[] = [];
|
||||
dfs(tree, node => lines.push(' '.repeat(level++) + `- ${node.kind} ${formatObject(node.params ?? {})}`), node => level--);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** Convert object to a human-friendly string (similar to JSON.stringify but without quoting keys) */
|
||||
export function formatObject(obj: {} | undefined): string {
|
||||
if (!obj) return 'undefined';
|
||||
return JSON.stringify(obj).replace(/,("\w+":)/g, ', $1').replace(/"(\w+)":/g, '$1: ');
|
||||
}
|
||||
|
||||
|
||||
/** Create a copy of a tree node, ignoring children. */
|
||||
export function copyNodeWithoutChildren<TTree extends Tree>(node: TTree): TTree {
|
||||
return {
|
||||
kind: node.kind,
|
||||
params: node.params ? { ...node.params } : undefined,
|
||||
} as TTree;
|
||||
}
|
||||
/** Create a copy of a tree node, including a shallow copy of children. */
|
||||
export function copyNode<TTree extends Tree>(node: TTree): TTree {
|
||||
return {
|
||||
kind: node.kind,
|
||||
params: node.params ? { ...node.params } : undefined,
|
||||
children: node.children ? [...node.children] : undefined,
|
||||
} as TTree;
|
||||
}
|
||||
|
||||
/** Create a deep copy of a tree. */
|
||||
export function copyTree<T extends Tree>(root: T): T {
|
||||
return convertTree(root, {}) as T;
|
||||
}
|
||||
|
||||
/** Set of rules for converting a tree of one schema into a different schema.
|
||||
* Each rule defines how to convert a node of a specific kind, e.g.
|
||||
* `{A: node => [], B: node => [{kind: 'X',...}], C: node => [{kind: 'Y',...}, {kind: 'Z',...}]}`:
|
||||
* nodes of kind `A` will be deleted (their children moved to parent),
|
||||
* nodes of kind `B` will be converted to kind `X`,
|
||||
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
|
||||
* nodes of other kinds will just be copied. */
|
||||
export type ConversionRules<A extends Tree, B extends Tree> = {
|
||||
[kind in Kind<SubTree<A>>]?: (node: SubTreeOfKind<A, kind>, parent?: SubTree<A>) => SubTree<B>[]
|
||||
};
|
||||
|
||||
/** Apply a set of conversion rules to a tree to change to a different schema. */
|
||||
export function convertTree<A extends Tree, B extends Tree>(root: A, conversions: ConversionRules<A, B>): SubTree<B> {
|
||||
const mapping = new Map<SubTree<A>, SubTree<B>>();
|
||||
let convertedRoot: SubTree<B>;
|
||||
dfs<A>(root, (node, parent) => {
|
||||
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: SubTree<A>) => SubTree<B>[]) | undefined;
|
||||
if (conversion) {
|
||||
const convertidos = conversion(node, parent);
|
||||
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
|
||||
let convParent = parent ? mapping.get(parent) : undefined;
|
||||
for (const conv of convertidos) {
|
||||
if (convParent) {
|
||||
(convParent.children ??= []).push(conv);
|
||||
} else {
|
||||
convertedRoot = conv;
|
||||
}
|
||||
convParent = conv;
|
||||
}
|
||||
mapping.set(node, convParent!);
|
||||
} else {
|
||||
const converted = copyNodeWithoutChildren(node);
|
||||
if (parent) {
|
||||
(mapping.get(parent)!.children ??= []).push(converted);
|
||||
} else {
|
||||
convertedRoot = converted;
|
||||
}
|
||||
mapping.set(node, converted);
|
||||
}
|
||||
});
|
||||
return convertedRoot!;
|
||||
}
|
||||
|
||||
/** Create a copy of the tree where twins (siblings of the same kind with the same params) are merged into one node.
|
||||
* Applies only to the node kinds listed in `condenseNodes` (or all if undefined) except node kinds in `skipNodes`. */
|
||||
export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<Tree>>, skipNodes?: Set<Kind<Tree>>): T {
|
||||
const map = new Map<string, SubTree<T>>();
|
||||
const result = copyTree(root);
|
||||
dfs<T>(result, node => {
|
||||
map.clear();
|
||||
const newChildren: SubTree<T>[] = [];
|
||||
for (const child of node.children ?? []) {
|
||||
let twin: SubTree<T> | undefined = undefined;
|
||||
const doApply = (!condenseNodes || condenseNodes.has(child.kind)) && !skipNodes?.has(child.kind);
|
||||
if (doApply) {
|
||||
const key = child.kind + canonicalJsonString(getParams(child));
|
||||
twin = map.get(key);
|
||||
if (!twin) map.set(key, child);
|
||||
}
|
||||
if (twin) {
|
||||
(twin.children ??= []).push(...child.children ?? []);
|
||||
} else {
|
||||
newChildren.push(child as SubTree<T>);
|
||||
}
|
||||
}
|
||||
node.children = newChildren;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Create a copy of the tree where missing optional params for each node are added based on `defaults`. */
|
||||
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, defaults: DefaultsForTree<S>): TreeFor<TreeSchemaWithAllRequired<S>> {
|
||||
const rules: ConversionRules<TreeFor<S>, TreeFor<S>> = {};
|
||||
for (const kind in defaults) {
|
||||
rules[kind] = node => [{ kind: node.kind, params: { ...defaults[kind], ...node.params } } as any];
|
||||
}
|
||||
return convertTree(tree, rules) as any;
|
||||
}
|
||||
|
||||
/** Resolve any URI params in a tree, in place. URI params are those listed in `uriParamNames`.
|
||||
* Relative URIs are treated as relative to `baseUri`, which can in turn be relative to the window URL (if available). */
|
||||
export function resolveUris<T extends Tree>(tree: T, baseUri: string, uriParamNames: string[]): void {
|
||||
dfs(tree, node => {
|
||||
const params = node.params as Record<string, any> | undefined;
|
||||
if (!params) return;
|
||||
for (const name in params) {
|
||||
if (uriParamNames.includes(name)) {
|
||||
const uri = params[name];
|
||||
if (typeof uri === 'string') {
|
||||
params[name] = resolveUri(uri, baseUri, windowUrl());
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Resolve a sequence of URI references (relative URIs), where each reference is either absolute or relative to the next one
|
||||
* (i.e. the last one is the base URI). Skip any `undefined`.
|
||||
* E.g. `resolveUri('./unexpected.png', '/spanish/inquisition/expectations.html', 'https://example.org/spam/spam/spam')`
|
||||
* returns `'https://example.org/spanish/inquisition/unexpected.png'`. */
|
||||
function resolveUri(...refs: (string | undefined)[]): string | undefined {
|
||||
let result: string | undefined = undefined;
|
||||
for (const ref of refs.reverse()) {
|
||||
if (ref !== undefined) {
|
||||
if (result === undefined) result = ref;
|
||||
else result = new URL(ref, result).href;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/** Return URL of the current page when running in a browser; `undefined` when running in Node. */
|
||||
function windowUrl(): string | undefined {
|
||||
return (typeof window !== 'undefined') ? window.location.href : undefined;
|
||||
}
|
||||
101
src/extensions/mvs/tree/molstar/conversion.ts
Normal file
101
src/extensions/mvs/tree/molstar/conversion.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
|
||||
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
|
||||
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
|
||||
import { MVSDefaults } from '../mvs/mvs-defaults';
|
||||
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
|
||||
|
||||
/** Convert `format` parameter of `parse` node in `MolstarTree`
|
||||
* into `format` and `is_binary` parameters in `MolstarTree` */
|
||||
export const ParseFormatMvsToMolstar = {
|
||||
mmcif: { format: 'cif', is_binary: false },
|
||||
bcif: { format: 'cif', is_binary: true },
|
||||
pdb: { format: 'pdb', is_binary: false },
|
||||
} satisfies { [p in ParseFormatT]: { format: MolstarParseFormatT, is_binary: boolean } };
|
||||
|
||||
|
||||
/** Conversion rules for conversion from `MVSTree` (with all parameter values) to `MolstarTree` */
|
||||
const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
|
||||
'download': node => [],
|
||||
'parse': (node, parent) => {
|
||||
const { format, is_binary } = ParseFormatMvsToMolstar[node.params.format];
|
||||
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format } };
|
||||
if (parent?.kind === 'download') {
|
||||
return [
|
||||
{ kind: 'download', params: { ...parent.params, is_binary } },
|
||||
convertedNode,
|
||||
] satisfies MolstarNode[];
|
||||
} else {
|
||||
console.warn('"parse" node is not being converted, this is suspicious');
|
||||
return [convertedNode] satisfies MolstarNode[];
|
||||
}
|
||||
},
|
||||
'structure': (node, parent) => {
|
||||
if (parent?.kind !== 'parse') throw new Error('Parent of "structure" must be "parse".');
|
||||
const { format } = ParseFormatMvsToMolstar[parent.params.format];
|
||||
return [
|
||||
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
|
||||
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
|
||||
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']) },
|
||||
] satisfies MolstarNode[];
|
||||
},
|
||||
};
|
||||
|
||||
/** Node kinds in `MolstarTree` that it makes sense to condense */
|
||||
const molstarNodesToCondense = new Set<MolstarKind>(['download', 'parse', 'trajectory', 'model'] satisfies MolstarKind[]);
|
||||
|
||||
/** Convert MolViewSpec tree into MolStar tree */
|
||||
export function convertMvsToMolstar(mvsTree: MVSTree, sourceUrl: string | undefined): MolstarTree {
|
||||
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSDefaults) as FullMVSTree;
|
||||
if (sourceUrl) resolveUris(full, sourceUrl, ['uri', 'url']);
|
||||
const converted = convertTree<FullMVSTree, MolstarTree>(full, mvsToMolstarConversionRules);
|
||||
if (converted.kind !== 'root') throw new Error("Root's type is not 'root' after conversion from MVS tree to Molstar tree.");
|
||||
const condensed = condenseTree<MolstarTree>(converted, molstarNodesToCondense);
|
||||
return condensed;
|
||||
}
|
||||
|
||||
|
||||
type FileExtension = `.${Lowercase<string>}`;
|
||||
function fileExtensionMatches(filename: string, extensions: (FileExtension | '*')[]): boolean {
|
||||
filename = filename.toLowerCase();
|
||||
return extensions.some(ext => ext === '*' || filename.endsWith(ext));
|
||||
}
|
||||
|
||||
const StructureFormatExtensions: Record<ParseFormatT, (FileExtension | '*')[]> = {
|
||||
mmcif: ['.cif', '.mmif'],
|
||||
bcif: ['.bcif'],
|
||||
pdb: ['.pdb', '.ent'],
|
||||
};
|
||||
|
||||
/** Run some sanity check on a MVSTree. Return a list of potential problems (`undefined` if there are none) */
|
||||
export function mvsSanityCheckIssues(tree: MVSTree): string[] | undefined {
|
||||
const result: string[] = [];
|
||||
dfs(tree, (node, parent) => {
|
||||
if (node.kind === 'parse' && parent?.kind === 'download') {
|
||||
const source = parent.params.url;
|
||||
const extensions = StructureFormatExtensions[node.params.format];
|
||||
if (!fileExtensionMatches(source, extensions)) {
|
||||
result.push(`Parsing data from ${source} as ${node.params.format} format might be a mistake. The file extension doesn't match recommended file extensions (${extensions.join(', ')})`);
|
||||
}
|
||||
}
|
||||
});
|
||||
return result.length > 0 ? result : undefined;
|
||||
}
|
||||
|
||||
/** Run some sanity check on a MVSTree and print potential issues to the console. */
|
||||
export function mvsSanityCheck(tree: MVSTree): void {
|
||||
const issues = mvsSanityCheckIssues(tree);
|
||||
if (issues) {
|
||||
console.warn('There are potential issues in the MVS tree:');
|
||||
for (const issue of issues) {
|
||||
console.warn(' ', issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
64
src/extensions/mvs/tree/molstar/molstar-tree.ts
Normal file
64
src/extensions/mvs/tree/molstar/molstar-tree.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { RequiredField, bool } from '../generic/params-schema';
|
||||
import { NodeFor, TreeFor, TreeSchema } from '../generic/tree-schema';
|
||||
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
|
||||
import { MolstarParseFormatT } from '../mvs/param-types';
|
||||
|
||||
|
||||
/** Schema for `MolstarTree` (intermediate tree representation between `MVSTree` and a real Molstar state) */
|
||||
export const MolstarTreeSchema = TreeSchema({
|
||||
rootKind: 'root',
|
||||
nodes: {
|
||||
...FullMVSTreeSchema.nodes,
|
||||
download: {
|
||||
...FullMVSTreeSchema.nodes.download,
|
||||
params: {
|
||||
...FullMVSTreeSchema.nodes.download.params,
|
||||
is_binary: RequiredField(bool),
|
||||
},
|
||||
},
|
||||
parse: {
|
||||
...FullMVSTreeSchema.nodes.parse,
|
||||
params: {
|
||||
format: RequiredField(MolstarParseFormatT),
|
||||
},
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
|
||||
trajectory: {
|
||||
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
|
||||
parent: ['parse'],
|
||||
params: {
|
||||
format: RequiredField(MolstarParseFormatT),
|
||||
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index'] as const),
|
||||
},
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
|
||||
model: {
|
||||
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
|
||||
parent: ['trajectory'],
|
||||
params: pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['model_index'] as const),
|
||||
},
|
||||
/** Auxiliary node corresponding to Molstar's StructureFromModel. */
|
||||
structure: {
|
||||
...FullMVSTreeSchema.nodes.structure,
|
||||
parent: ['model'],
|
||||
params: omitObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index', 'model_index'] as const),
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** Node kind in a `MolstarTree` */
|
||||
export type MolstarKind = keyof typeof MolstarTreeSchema.nodes;
|
||||
|
||||
/** Node in a `MolstarTree` */
|
||||
export type MolstarNode<TKind extends MolstarKind = MolstarKind> = NodeFor<typeof MolstarTreeSchema, TKind>
|
||||
|
||||
/** Intermediate tree representation between `MVSTree` and a real Molstar state */
|
||||
export type MolstarTree = TreeFor<typeof MolstarTreeSchema>
|
||||
19
src/extensions/mvs/tree/mvs/_spec/mvs-builder.spec.ts
Normal file
19
src/extensions/mvs/tree/mvs/_spec/mvs-builder.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { treeValidationIssues } from '../../generic/tree-schema';
|
||||
import { builderDemo } from '../mvs-builder';
|
||||
import { MVSTreeSchema } from '../mvs-tree';
|
||||
|
||||
|
||||
describe('mvs-builder', () => {
|
||||
it('mvs-builder demo works', async () => {
|
||||
const mvsData = builderDemo();
|
||||
expect(typeof mvsData.metadata.version).toEqual('string');
|
||||
expect(typeof mvsData.metadata.timestamp).toEqual('string');
|
||||
expect(treeValidationIssues(MVSTreeSchema, mvsData.root)).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
261
src/extensions/mvs/tree/mvs/mvs-builder.ts
Normal file
261
src/extensions/mvs/tree/mvs/mvs-builder.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
|
||||
import { MVSData } from '../../mvs-data';
|
||||
import { ParamsOfKind, SubTreeOfKind } from '../generic/tree-schema';
|
||||
import { MVSDefaults } from './mvs-defaults';
|
||||
import { MVSKind, MVSNode, MVSTree, MVSTreeSchema } from './mvs-tree';
|
||||
|
||||
|
||||
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
|
||||
*
|
||||
* ```
|
||||
* const builder = createMVSBuilder();
|
||||
* builder.canvas({ background_color: 'white' });
|
||||
* const struct = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure();
|
||||
* struct.component().representation().color({ color: '#3050F8' });
|
||||
* console.log(JSON.stringify(builder.getState()));
|
||||
* ```
|
||||
*/
|
||||
export function createMVSBuilder() {
|
||||
return new Root();
|
||||
}
|
||||
|
||||
|
||||
/** Base class for MVS builder pointing to anything */
|
||||
class _Base<TKind extends MVSKind> {
|
||||
protected constructor(
|
||||
protected readonly _root: Root,
|
||||
protected readonly _node: SubTreeOfKind<MVSTree, TKind>,
|
||||
) { }
|
||||
/** Create a new node, append as child to current _node, and return the new node */
|
||||
protected addChild<TChildKind extends MVSKind>(kind: TChildKind, params: ParamsOfKind<MVSTree, TChildKind>) {
|
||||
const allowedParamNames = Object.keys(MVSTreeSchema.nodes[kind].params) as (keyof ParamsOfKind<MVSTree, TChildKind>)[];
|
||||
const node = {
|
||||
kind,
|
||||
params: pickObjectKeys(params, allowedParamNames) as unknown,
|
||||
} as SubTreeOfKind<MVSTree, TChildKind>;
|
||||
this._node.children ??= [];
|
||||
this._node.children.push(node);
|
||||
return node;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to the 'root' node */
|
||||
export class Root extends _Base<'root'> {
|
||||
constructor() {
|
||||
const node: MVSNode<'root'> = { kind: 'root' };
|
||||
super(undefined as any, node);
|
||||
(this._root as Root) = this;
|
||||
}
|
||||
/** Return the current state of the builder as object in MVS format. */
|
||||
getState(metadata?: Partial<Pick<MVSData['metadata'], 'title' | 'description' | 'description_format'>>): MVSData {
|
||||
return {
|
||||
root: deepClone(this._node),
|
||||
metadata: {
|
||||
...metadata,
|
||||
version: `${MVSData.SupportedVersion}`,
|
||||
timestamp: utcNowISO(),
|
||||
},
|
||||
};
|
||||
}
|
||||
// omitting `saveState`, filesystem operations are responsibility of the caller code (platform-dependent)
|
||||
|
||||
/** Add a 'camera' node and return builder pointing to the root. 'camera' node instructs to set the camera position and orientation. */
|
||||
camera(params: ParamsOfKind<MVSTree, 'camera'>): Root {
|
||||
this.addChild('camera', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'canvas' node and return builder pointing to the root. 'canvas' node sets canvas properties. */
|
||||
canvas(params: ParamsOfKind<MVSTree, 'canvas'>): Root {
|
||||
this.addChild('canvas', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'download' node and return builder pointing to it. 'download' node instructs to retrieve a data resource. */
|
||||
download(params: ParamsOfKind<MVSTree, 'download'>): Download {
|
||||
return new Download(this._root, this.addChild('download', params));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'download' node */
|
||||
export class Download extends _Base<'download'> {
|
||||
/** Add a 'parse' node and return builder pointing to it. 'parse' node instructs to parse a data resource. */
|
||||
parse(params: ParamsOfKind<MVSTree, 'parse'>) {
|
||||
return new Parse(this._root, this.addChild('parse', params));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Subsets of 'structure' node params which will be passed to individual builder functions. */
|
||||
const StructureParamsSubsets = {
|
||||
model: ['block_header', 'block_index', 'model_index'],
|
||||
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id'],
|
||||
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max'],
|
||||
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius'],
|
||||
} satisfies { [kind in ParamsOfKind<MVSTree, 'structure'>['type']]: (keyof ParamsOfKind<MVSTree, 'structure'>)[] };
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'parse' node */
|
||||
export class Parse extends _Base<'parse'> {
|
||||
/** Add a 'structure' node representing a "model structure", i.e. includes all coordinates from the original model without applying any transformations.
|
||||
* Return builder pointing to the new node. */
|
||||
modelStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['model'][number]> = {}): Structure {
|
||||
return new Structure(this._root, this.addChild('structure', {
|
||||
type: 'model',
|
||||
...pickObjectKeys(params, StructureParamsSubsets.model),
|
||||
}));
|
||||
}
|
||||
/** Add a 'structure' node representing an "assembly structure", i.e. may apply filters and symmetry operators to the original model coordinates.
|
||||
* Return builder pointing to the new node. */
|
||||
assemblyStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['assembly'][number]> = {}): Structure {
|
||||
return new Structure(this._root, this.addChild('structure', {
|
||||
type: 'assembly',
|
||||
...pickObjectKeys(params, StructureParamsSubsets.assembly),
|
||||
}));
|
||||
}
|
||||
/** Add a 'structure' node representing a "symmetry structure", i.e. applies symmetry operators to build crystal unit cells within given Miller indices.
|
||||
* Return builder pointing to the new node. */
|
||||
symmetryStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['symmetry'][number]> = {}): Structure {
|
||||
return new Structure(this._root, this.addChild('structure', {
|
||||
type: 'symmetry',
|
||||
...pickObjectKeys(params, StructureParamsSubsets.symmetry),
|
||||
}));
|
||||
}
|
||||
/** Add a 'structure' node representing a "symmetry mates structure", i.e. applies symmetry operators to build asymmetric units within a radius from the original model.
|
||||
* Return builder pointing to the new node. */
|
||||
symmetryMatesStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['symmetry_mates'][number]> = {}): Structure {
|
||||
return new Structure(this._root, this.addChild('structure', {
|
||||
type: 'symmetry_mates',
|
||||
...pickObjectKeys(params, StructureParamsSubsets.symmetry_mates),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'structure' node */
|
||||
export class Structure extends _Base<'structure'> {
|
||||
/** Add a 'component' node and return builder pointing to it. 'component' node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component(params: Partial<ParamsOfKind<MVSTree, 'component'>> = {}): Component {
|
||||
const fullParams = { ...params, selector: params.selector ?? MVSDefaults.component.selector };
|
||||
return new Component(this._root, this.addChild('component', fullParams));
|
||||
}
|
||||
/** Add a 'component_from_uri' node and return builder pointing to it. 'component_from_uri' node instructs to create a component defined by an external annotation resource. */
|
||||
componentFromUri(params: ParamsOfKind<MVSTree, 'component_from_uri'>): Component {
|
||||
return new Component(this._root, this.addChild('component_from_uri', params));
|
||||
}
|
||||
/** Add a 'component_from_source' node and return builder pointing to it. 'component_from_source' node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
componentFromSource(params: ParamsOfKind<MVSTree, 'component_from_source'>): Component {
|
||||
return new Component(this._root, this.addChild('component_from_source', params));
|
||||
}
|
||||
/** Add a 'label_from_uri' node and return builder pointing back to the structure node. 'label_from_uri' node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
|
||||
labelFromUri(params: ParamsOfKind<MVSTree, 'label_from_uri'>): Structure {
|
||||
this.addChild('label_from_uri', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'label_from_source' node and return builder pointing back to the structure node. 'label_from_source' node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
labelFromSource(params: ParamsOfKind<MVSTree, 'label_from_source'>): Structure {
|
||||
this.addChild('label_from_source', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'tooltip_from_uri' node and return builder pointing back to the structure node. 'tooltip_from_uri' node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
|
||||
tooltipFromUri(params: ParamsOfKind<MVSTree, 'tooltip_from_uri'>): Structure {
|
||||
this.addChild('tooltip_from_uri', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'tooltip_from_source' node and return builder pointing back to the structure node. 'tooltip_from_source' node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
tooltipFromSource(params: ParamsOfKind<MVSTree, 'tooltip_from_source'>): Structure {
|
||||
this.addChild('tooltip_from_source', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'transform' node and return builder pointing back to the structure node. 'transform' node instructs to rotate and/or translate structure coordinates. */
|
||||
transform(params: ParamsOfKind<MVSTree, 'transform'> = {}): Structure {
|
||||
if (params.rotation && params.rotation.length !== 9) {
|
||||
throw new Error('ValueError: `rotation` parameter must be an array of 9 numbers');
|
||||
}
|
||||
this.addChild('transform', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
|
||||
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> {
|
||||
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
|
||||
representation(params: Partial<ParamsOfKind<MVSTree, 'representation'>> = {}): Representation {
|
||||
const fullParams: ParamsOfKind<MVSTree, 'representation'> = { ...params, type: params.type ?? 'cartoon' };
|
||||
return new Representation(this._root, this.addChild('representation', fullParams));
|
||||
}
|
||||
/** Add a 'label' node and return builder pointing back to the component node. 'label' node instructs to add a label (textual visual representation) to a component. */
|
||||
label(params: ParamsOfKind<MVSTree, 'label'>): Component {
|
||||
this.addChild('label', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'tooltip' node and return builder pointing back to the component node. 'tooltip' node instructs to add a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
|
||||
tooltip(params: ParamsOfKind<MVSTree, 'tooltip'>): Component {
|
||||
this.addChild('tooltip', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'focus' node and return builder pointing back to the component node. 'focus' node instructs to set the camera focus to a component (zoom in). */
|
||||
focus(params: ParamsOfKind<MVSTree, 'focus'> = {}): Component {
|
||||
this.addChild('focus', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** MVS builder pointing to a 'representation' node */
|
||||
export class Representation extends _Base<'representation'> {
|
||||
/** Add a 'color' node and return builder pointing back to the representation node. 'color' node instructs to apply color to a visual representation. */
|
||||
color(params: ParamsOfKind<MVSTree, 'color'>): Representation {
|
||||
this.addChild('color', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'color_from_uri' node and return builder pointing back to the representation node. 'color_from_uri' node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
|
||||
colorFromUri(params: ParamsOfKind<MVSTree, 'color_from_uri'>): Representation {
|
||||
this.addChild('color_from_uri', params);
|
||||
return this;
|
||||
}
|
||||
/** Add a 'color_from_source' node and return builder pointing back to the representation node. 'color_from_source' node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
colorFromSource(params: ParamsOfKind<MVSTree, 'color_from_source'>): Representation {
|
||||
this.addChild('color_from_source', params);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Demonstration of usage of MVS builder */
|
||||
export function builderDemo() {
|
||||
const builder = createMVSBuilder();
|
||||
builder.canvas({ background_color: 'white' });
|
||||
const struct = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure();
|
||||
struct.component().representation().color({ color: 'white' });
|
||||
struct.component({ selector: 'ligand' }).representation({ type: 'ball_and_stick' })
|
||||
.color({ color: '#555555' })
|
||||
.color({ selector: { type_symbol: 'N' }, color: '#3050F8' })
|
||||
.color({ selector: { type_symbol: 'O' }, color: '#FF0D0D' })
|
||||
.color({ selector: { type_symbol: 'S' }, color: '#FFFF30' })
|
||||
.color({ selector: { type_symbol: 'FE' }, color: '#E06633' });
|
||||
builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og5_updated.cif' }).parse({ format: 'mmcif' }).assemblyStructure({ assembly_id: '1' }).component().representation().color({ color: 'cyan' });
|
||||
builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og5_updated.cif' }).parse({ format: 'mmcif' }).assemblyStructure({ assembly_id: '2' }).component().representation().color({ color: 'blue' });
|
||||
const cif = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1wrf_updated.cif' }).parse({ format: 'mmcif' });
|
||||
|
||||
cif.modelStructure({ model_index: 0 }).component().representation().color({ color: '#CC0000' });
|
||||
cif.modelStructure({ model_index: 1 }).component().representation().color({ color: '#EE7700' });
|
||||
cif.modelStructure({ model_index: 2 }).component().representation().color({ color: '#FFFF00' });
|
||||
|
||||
cif.modelStructure({ model_index: 0 }).transform({ translation: [30, 0, 0] }).component().representation().color({ color: '#ff88bb' });
|
||||
cif.modelStructure({ model_index: 0 as any }).transform({ translation: [60, 0, 0], rotation: [0, 1, 0, -1, 0, 0, 0, 0, 1] }).component().representation().color({ color: '#aa0077' });
|
||||
|
||||
return builder.getState();
|
||||
}
|
||||
|
||||
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
|
||||
function utcNowISO(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
105
src/extensions/mvs/tree/mvs/mvs-defaults.ts
Normal file
105
src/extensions/mvs/tree/mvs/mvs-defaults.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { DefaultsForTree } from '../generic/tree-schema';
|
||||
import { MVSTreeSchema } from './mvs-tree';
|
||||
|
||||
|
||||
/** Default values for params in `MVSTree` */
|
||||
export const MVSDefaults = {
|
||||
root: {},
|
||||
download: {
|
||||
},
|
||||
parse: {
|
||||
},
|
||||
structure: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
model_index: 0,
|
||||
assembly_id: null,
|
||||
radius: 5,
|
||||
ijk_min: [-1, -1, -1],
|
||||
ijk_max: [1, 1, 1],
|
||||
},
|
||||
component: {
|
||||
selector: 'all' as const,
|
||||
},
|
||||
component_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'component',
|
||||
field_values: null,
|
||||
},
|
||||
component_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'component',
|
||||
field_values: null,
|
||||
},
|
||||
representation: {
|
||||
},
|
||||
color: {
|
||||
selector: 'all' as const,
|
||||
},
|
||||
color_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'color',
|
||||
},
|
||||
color_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'color',
|
||||
},
|
||||
label: {
|
||||
},
|
||||
label_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'label',
|
||||
},
|
||||
label_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'label',
|
||||
},
|
||||
tooltip: {
|
||||
},
|
||||
tooltip_from_uri: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'tooltip',
|
||||
},
|
||||
tooltip_from_source: {
|
||||
block_header: null,
|
||||
block_index: 0,
|
||||
category_name: null,
|
||||
field_name: 'tooltip',
|
||||
},
|
||||
focus: {
|
||||
direction: [0, 0, -1],
|
||||
up: [0, 1, 0],
|
||||
},
|
||||
transform: {
|
||||
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1], // 3x3 identitity matrix
|
||||
translation: [0, 0, 0],
|
||||
},
|
||||
canvas: {
|
||||
},
|
||||
camera: {
|
||||
up: [0, 1, 0],
|
||||
},
|
||||
} satisfies DefaultsForTree<typeof MVSTreeSchema>;
|
||||
|
||||
/** Color to be used e.g. for representations without 'color' node */
|
||||
export const DefaultColor = 'white';
|
||||
272
src/extensions/mvs/tree/mvs/mvs-tree.ts
Normal file
272
src/extensions/mvs/tree/mvs/mvs-tree.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import { OptionalField, RequiredField, float, int, list, nullable, str, tuple, union } from '../generic/params-schema';
|
||||
import { NodeFor, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
|
||||
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StructureTypeT, Vector3 } from './param-types';
|
||||
|
||||
|
||||
const _DataFromUriParams = {
|
||||
/** URL of the annotation resource. */
|
||||
uri: RequiredField(str, 'URL of the annotation resource.'),
|
||||
/** Format of the annotation resource. */
|
||||
format: RequiredField(SchemaFormatT, 'Format of the annotation resource.'),
|
||||
/** Annotation schema defines what fields in the annotation will be taken into account. */
|
||||
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
|
||||
/** Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`). */
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
|
||||
/** Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used. */
|
||||
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
};
|
||||
|
||||
const _DataFromSourceParams = {
|
||||
/** Annotation schema defines what fields in the annotation will be taken into account. */
|
||||
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
|
||||
/** Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`). */
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
|
||||
/** Name of the CIF category to read annotation from. If `null`, the first category in the block is used. */
|
||||
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
|
||||
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
|
||||
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
|
||||
};
|
||||
|
||||
|
||||
/** Schema for `MVSTree` (MolViewSpec tree) */
|
||||
export const MVSTreeSchema = TreeSchema({
|
||||
rootKind: 'root',
|
||||
nodes: {
|
||||
/** Auxiliary node kind that only appears as the tree root. */
|
||||
root: {
|
||||
description: 'Auxiliary node kind that only appears as the tree root.',
|
||||
parent: [],
|
||||
params: {
|
||||
},
|
||||
},
|
||||
/** This node instructs to retrieve a data resource. */
|
||||
download: {
|
||||
description: 'This node instructs to retrieve a data resource.',
|
||||
parent: ['root'],
|
||||
params: {
|
||||
/** URL of the data resource. */
|
||||
url: RequiredField(str, 'URL of the data resource.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to parse a data resource. */
|
||||
parse: {
|
||||
description: 'This node instructs to parse a data resource.',
|
||||
parent: ['download'],
|
||||
params: {
|
||||
/** Format of the input data resource. */
|
||||
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
|
||||
structure: {
|
||||
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
|
||||
parent: ['parse'],
|
||||
params: {
|
||||
/** Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model). */
|
||||
type: RequiredField(StructureTypeT, 'Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model).'),
|
||||
/** Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`. */
|
||||
block_header: OptionalField(nullable(str), 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
|
||||
/** 0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`). */
|
||||
block_index: OptionalField(int, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
|
||||
/** 0-based index of model in case the input data contain multiple models. */
|
||||
model_index: OptionalField(int, '0-based index of model in case the input data contain multiple models.'),
|
||||
/** Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected. */
|
||||
assembly_id: OptionalField(nullable(str), 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
|
||||
/** Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`). */
|
||||
radius: OptionalField(float, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
|
||||
/** Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_min: OptionalField(tuple([int, int, int]), 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
|
||||
ijk_max: OptionalField(tuple([int, int, int]), 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to rotate and/or translate structure coordinates. */
|
||||
transform: {
|
||||
description: 'This node instructs to rotate and/or translate structure coordinates.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
|
||||
rotation: OptionalField(Matrix, 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
|
||||
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
|
||||
translation: OptionalField(Vector3, 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component (i.e. a subset of the parent structure). */
|
||||
component: {
|
||||
description: 'This node instructs to create a component (i.e. a subset of the parent structure).',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
/** Defines what part of the parent structure should be included in this component. */
|
||||
selector: RequiredField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines what part of the parent structure should be included in this component.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component defined by an external annotation resource. */
|
||||
component_from_uri: {
|
||||
description: 'This node instructs to create a component defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
|
||||
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
component_from_source: {
|
||||
description: 'This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
|
||||
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to create a visual representation of a component. */
|
||||
representation: {
|
||||
description: 'This node instructs to create a visual representation of a component.',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: {
|
||||
/** Method of visual representation of the component. */
|
||||
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply color to a visual representation. */
|
||||
color: {
|
||||
description: 'This node instructs to apply color to a visual representation.',
|
||||
parent: ['representation'],
|
||||
params: {
|
||||
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
/** Defines to what part of the representation this color should be applied. */
|
||||
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines to what part of the representation this color should be applied.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
|
||||
color_from_uri: {
|
||||
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource.',
|
||||
parent: ['representation'],
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
color_from_source: {
|
||||
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['representation'],
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to add a label (textual visual representation) to a component. */
|
||||
label: {
|
||||
description: 'This node instructs to add a label (textual visual representation) to a component.',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: {
|
||||
/** Content of the shown label. */
|
||||
text: RequiredField(str, 'Content of the shown label.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
|
||||
label_from_uri: {
|
||||
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
label_from_source: {
|
||||
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
|
||||
tooltip: {
|
||||
description: 'This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component).',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: {
|
||||
/** Content of the shown tooltip. */
|
||||
text: RequiredField(str, 'Content of the shown tooltip.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
|
||||
tooltip_from_uri: {
|
||||
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromUriParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
|
||||
tooltip_from_source: {
|
||||
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
|
||||
parent: ['structure'],
|
||||
params: {
|
||||
..._DataFromSourceParams,
|
||||
},
|
||||
},
|
||||
/** This node instructs to set the camera focus to a component (zoom in). */
|
||||
focus: {
|
||||
description: 'This node instructs to set the camera focus to a component (zoom in).',
|
||||
parent: ['component', 'component_from_uri', 'component_from_source'],
|
||||
params: {
|
||||
/** Vector describing the direction of the view (camera position -> focused target). */
|
||||
direction: OptionalField(Vector3, 'Vector describing the direction of the view (camera position -> focused target).'),
|
||||
/** Vector which will be aligned with the screen Y axis. */
|
||||
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
|
||||
},
|
||||
},
|
||||
/** This node instructs to set the camera position and orientation. */
|
||||
camera: {
|
||||
description: 'This node instructs to set the camera position and orientation.',
|
||||
parent: ['root'],
|
||||
params: {
|
||||
/** Coordinates of the point in space at which the camera is pointing. */
|
||||
target: RequiredField(Vector3, 'Coordinates of the point in space at which the camera is pointing.'),
|
||||
/** Coordinates of the camera. */
|
||||
position: RequiredField(Vector3, 'Coordinates of the camera.'),
|
||||
/** Vector which will be aligned with the screen Y axis. */
|
||||
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
|
||||
},
|
||||
},
|
||||
/** This node sets canvas properties. */
|
||||
canvas: {
|
||||
description: 'This node sets canvas properties.',
|
||||
parent: ['root'],
|
||||
params: {
|
||||
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
|
||||
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/** Node kind in a `MVSTree` */
|
||||
export type MVSKind = keyof typeof MVSTreeSchema.nodes
|
||||
|
||||
/** Node in a `MVSTree` */
|
||||
export type MVSNode<TKind extends MVSKind = MVSKind> = NodeFor<typeof MVSTreeSchema, TKind>
|
||||
|
||||
/** MolViewSpec tree */
|
||||
export type MVSTree = TreeFor<typeof MVSTreeSchema>
|
||||
|
||||
|
||||
/** Schema for `MVSTree` (MolViewSpec tree with all params provided) */
|
||||
export const FullMVSTreeSchema = TreeSchemaWithAllRequired(MVSTreeSchema);
|
||||
|
||||
/** MolViewSpec tree with all params provided */
|
||||
export type FullMVSTree = TreeFor<typeof FullMVSTreeSchema>
|
||||
73
src/extensions/mvs/tree/mvs/param-types.ts
Normal file
73
src/extensions/mvs/tree/mvs/param-types.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Adam Midlik <midlik@gmail.com>
|
||||
*/
|
||||
|
||||
import * as iots from 'io-ts';
|
||||
import { HexColor } from '../../helpers/utils';
|
||||
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/params-schema';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
|
||||
|
||||
/** `format` parameter values for `parse` node in MVS tree */
|
||||
export const ParseFormatT = literal('mmcif', 'bcif', 'pdb');
|
||||
export type ParseFormatT = ValueFor<typeof ParseFormatT>
|
||||
|
||||
/** `format` parameter values for `parse` node in Molstar tree */
|
||||
export const MolstarParseFormatT = literal('cif', 'pdb');
|
||||
export type MolstarParseFormatT = ValueFor<typeof MolstarParseFormatT>
|
||||
|
||||
/** `kind` parameter values for `structure` node in MVS tree */
|
||||
export const StructureTypeT = literal('model', 'assembly', 'symmetry', 'symmetry_mates');
|
||||
|
||||
/** `selector` parameter values for `component` node in MVS tree */
|
||||
export const ComponentSelectorT = literal('all', 'polymer', 'protein', 'nucleic', 'branched', 'ligand', 'ion', 'water');
|
||||
|
||||
/** `selector` parameter values for `component` node in MVS tree */
|
||||
export const ComponentExpressionT = iots.partial({
|
||||
label_entity_id: str,
|
||||
label_asym_id: str,
|
||||
auth_asym_id: str,
|
||||
label_seq_id: int,
|
||||
auth_seq_id: int,
|
||||
pdbx_PDB_ins_code: str,
|
||||
beg_label_seq_id: int,
|
||||
end_label_seq_id: int,
|
||||
beg_auth_seq_id: int,
|
||||
end_auth_seq_id: int,
|
||||
label_atom_id: str,
|
||||
auth_atom_id: str,
|
||||
type_symbol: str,
|
||||
atom_id: int,
|
||||
atom_index: int,
|
||||
});
|
||||
|
||||
/** `type` parameter values for `representation` node in MVS tree */
|
||||
export const RepresentationTypeT = literal('ball_and_stick', 'cartoon', 'surface');
|
||||
|
||||
/** `schema` parameter values for `*_from_uri` and `*_from_source` nodes in MVS tree */
|
||||
export const SchemaT = literal('whole_structure', 'entity', 'chain', 'auth_chain', 'residue', 'auth_residue', 'residue_range', 'auth_residue_range', 'atom', 'auth_atom', 'all_atomic');
|
||||
|
||||
/** `format` parameter values for `*_from_uri` nodes in MVS tree */
|
||||
export const SchemaFormatT = literal('cif', 'bcif', 'json');
|
||||
|
||||
/** Parameter values for vector params, e.g. `position` */
|
||||
export const Vector3 = tuple([float, float, float]);
|
||||
|
||||
/** Parameter values for matrix params, e.g. `rotation` */
|
||||
export const Matrix = list(float);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const HexColorT = new iots.Type<HexColor>(
|
||||
'HexColor',
|
||||
((value: any) => typeof value === 'string') as any,
|
||||
(value, ctx) => HexColor.is(value) ? { _tag: 'Right', right: value } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid hex color string` }] },
|
||||
value => value
|
||||
);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNames)[]);
|
||||
|
||||
/** `color` parameter values for `color` node in MVS tree */
|
||||
export const ColorT = union([HexColorT, ColorNamesT]);
|
||||
@@ -5,12 +5,13 @@
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider } from './prop';
|
||||
import { AssemblySymmetryProvider, AssemblySymmetry, AssemblySymmetryDataProvider, AssemblySymmetryDataParams } from './prop';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
|
||||
import { AssemblySymmetryParams, AssemblySymmetryRepresentation } from './representation';
|
||||
import { AssemblySymmetryClusterColorThemeProvider } from './color';
|
||||
import { PluginStateTransform, PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { PluginConfigItem } from '../../../mol-plugin/config';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateTransformer, StateAction, StateObject, StateTransform, StateObjectRef } from '../../../mol-state';
|
||||
import { GenericRepresentationRef } from '../../../mol-plugin-state/manager/structure/hierarchy-state';
|
||||
@@ -77,14 +78,15 @@ export const InitAssemblySymmetry3D = StateAction.build({
|
||||
description: 'Initialize Assembly Symmetry axes and cage. Data calculated with BioJava, obtained via RCSB PDB.'
|
||||
},
|
||||
from: PluginStateObject.Molecule.Structure,
|
||||
isApplicable: (a) => AssemblySymmetry.isApplicable(a.data)
|
||||
})(({ a, ref, state }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
|
||||
isApplicable: (a) => AssemblySymmetry.isApplicable(a.data),
|
||||
params: (a, plugin: PluginContext) => getConfiguredDefaultParams(plugin)
|
||||
})(({ a, ref, state, params }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
|
||||
try {
|
||||
const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, a.data);
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, a.data, params);
|
||||
const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
|
||||
const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
|
||||
await AssemblySymmetryProvider.attach(propCtx, a.data, { symmetryIndex });
|
||||
await AssemblySymmetryProvider.attach(propCtx, a.data, { ...params, symmetryIndex });
|
||||
} catch (e) {
|
||||
plugin.log.error(`Assembly Symmetry: ${e}`);
|
||||
return;
|
||||
@@ -152,10 +154,6 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
|
||||
|
||||
//
|
||||
|
||||
export const AssemblySymmetryPresetParams = {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
};
|
||||
|
||||
export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
|
||||
id: 'preset-structure-representation-rcsb-assembly-symmetry',
|
||||
display: {
|
||||
@@ -165,7 +163,12 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
|
||||
isApplicable(a) {
|
||||
return AssemblySymmetry.isApplicable(a.data);
|
||||
},
|
||||
params: () => AssemblySymmetryPresetParams,
|
||||
params: (a, plugin) => {
|
||||
return {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
...getConfiguredDefaultParams(plugin)
|
||||
};
|
||||
},
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
@@ -174,15 +177,16 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
|
||||
if (!AssemblySymmetryDataProvider.get(structure).value) {
|
||||
await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
|
||||
const propCtx = { runtime, assetManager: plugin.managers.asset };
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, structure);
|
||||
const propProps = { serverType: params.serverType, serverUrl: params.serverUrl };
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, structure, propProps);
|
||||
const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
|
||||
const symmetryIndex = assemblySymmetryData ? AssemblySymmetry.firstNonC1(assemblySymmetryData) : -1;
|
||||
await AssemblySymmetryProvider.attach(propCtx, structure, { symmetryIndex });
|
||||
await AssemblySymmetryProvider.attach(propCtx, structure, { ...propProps, symmetryIndex });
|
||||
}));
|
||||
}
|
||||
|
||||
const assemblySymmetry = await tryCreateAssemblySymmetry(plugin, structureCell);
|
||||
const colorTheme = assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
|
||||
const colorTheme = getRCSBAssemblySymmetryConfig(plugin).ApplyColors && assemblySymmetry.isOk ? Tag.Cluster as any : undefined;
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params, theme: { globalName: colorTheme, focus: { name: colorTheme } } }, plugin);
|
||||
|
||||
return { components: preset.components, representations: { ...preset.representations, assemblySymmetry } };
|
||||
@@ -194,4 +198,27 @@ export function tryCreateAssemblySymmetry(plugin: PluginContext, structure: Stat
|
||||
const assemblySymmetry = state.build().to(structure)
|
||||
.applyOrUpdateTagged(AssemblySymmetry.Tag.Representation, AssemblySymmetry3D, params, { state: initialState });
|
||||
return assemblySymmetry.commit({ revertOnError: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
export const RCSBAssemblySymmetryConfig = {
|
||||
DefaultServerType: new PluginConfigItem('rcsb-assembly-symmetry.server-type', AssemblySymmetryDataParams.serverType.defaultValue),
|
||||
DefaultServerUrl: new PluginConfigItem('rcsb-assembly-symmetry.server-url', AssemblySymmetryDataParams.serverUrl.defaultValue),
|
||||
ApplyColors: new PluginConfigItem('rcsb-assembly-symmetry.apply-colors', true),
|
||||
};
|
||||
|
||||
export function getRCSBAssemblySymmetryConfig(plugin: PluginContext): { [key in keyof typeof RCSBAssemblySymmetryConfig]: NonNullable<typeof RCSBAssemblySymmetryConfig[key]['defaultValue']> } {
|
||||
return {
|
||||
ApplyColors: plugin.config.get(RCSBAssemblySymmetryConfig.ApplyColors) ?? RCSBAssemblySymmetryConfig.ApplyColors.defaultValue ?? true,
|
||||
DefaultServerType: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerType) ?? RCSBAssemblySymmetryConfig.DefaultServerType.defaultValue ?? AssemblySymmetryDataParams.serverType.defaultValue,
|
||||
DefaultServerUrl: plugin.config.get(RCSBAssemblySymmetryConfig.DefaultServerUrl) ?? RCSBAssemblySymmetryConfig.DefaultServerUrl.defaultValue ?? AssemblySymmetryDataParams.serverUrl.defaultValue,
|
||||
};
|
||||
}
|
||||
|
||||
function getConfiguredDefaultParams(plugin: PluginContext) {
|
||||
const config = getRCSBAssemblySymmetryConfig(plugin);
|
||||
const params = PD.clone(AssemblySymmetryDataParams);
|
||||
PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { SetUtils } from '../../../mol-util/set';
|
||||
import { MolScriptBuilder as MS } from '../../../mol-script/language/builder';
|
||||
import { compile } from '../../../mol-script/runtime/query/compiler';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
|
||||
const BiologicalAssemblyNames = new Set([
|
||||
'author_and_software_defined_assembly',
|
||||
@@ -48,7 +49,7 @@ export namespace AssemblySymmetry {
|
||||
Representation = 'rcsb-assembly-symmetry-3d'
|
||||
}
|
||||
|
||||
export const DefaultServerUrl = 'https://data.rcsb.org/graphql';
|
||||
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 (
|
||||
@@ -61,6 +62,8 @@ export namespace AssemblySymmetry {
|
||||
export async function fetch(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
|
||||
if (!isApplicable(structure)) return { value: [] };
|
||||
|
||||
if (props.serverType === 'pdbe') return fetch_PDBe(ctx, structure, props);
|
||||
|
||||
const client = new GraphQLClient(props.serverUrl, ctx.assetManager);
|
||||
const variables: AssemblySymmetryQueryVariables = {
|
||||
assembly_id: structure.units[0].conformation.operator.assembly?.id || '',
|
||||
@@ -77,6 +80,37 @@ export namespace AssemblySymmetry {
|
||||
return { value, assets: [result] };
|
||||
}
|
||||
|
||||
async function fetch_PDBe(ctx: CustomProperty.Context, structure: Structure, props: AssemblySymmetryDataProps): Promise<CustomProperty.Data<AssemblySymmetryDataValue>> {
|
||||
const assembly_id = structure.units[0].conformation.operator.assembly?.id || '-1'; // should use '' instead of '-1' but the API does not support non-number assembly_id
|
||||
const entry_id = structure.units[0].model.entryId.toLowerCase();
|
||||
const url = `${props.serverUrl}/${entry_id}?assembly_id=${assembly_id}`;
|
||||
const asset = Asset.getUrlAsset(ctx.assetManager, url);
|
||||
let dataWrapper: Asset.Wrapper<'json'>;
|
||||
try {
|
||||
dataWrapper = await ctx.assetManager.resolve(asset, 'json').runInContext(ctx.runtime);
|
||||
} catch (err) {
|
||||
// PDBe API returns 404 when there are no symmetries -> treat as success with empty json in body
|
||||
if (`${err}`.includes('404')) { // dirrrty
|
||||
dataWrapper = Asset.Wrapper({}, asset, ctx.assetManager);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
const data = dataWrapper.data;
|
||||
|
||||
const value: AssemblySymmetryDataValue = (data[entry_id] ?? []).map((v: any) => ({
|
||||
kind: 'Global Symmetry',
|
||||
oligomeric_state: v.oligomeric_state,
|
||||
stoichiometry: [v.stoichiometry],
|
||||
symbol: v.symbol,
|
||||
type: v.type,
|
||||
clusters: [],
|
||||
rotation_axes: v.rotation_axes,
|
||||
}));
|
||||
|
||||
return { value, assets: [dataWrapper] };
|
||||
}
|
||||
|
||||
/** Returns the index of the first non C1 symmetry or -1 */
|
||||
export function firstNonC1(assemblySymmetryData: AssemblySymmetryDataValue) {
|
||||
for (let i = 0, il = assemblySymmetryData.length; i < il; ++i) {
|
||||
@@ -147,7 +181,8 @@ export function getSymmetrySelectParam(structure?: Structure) {
|
||||
//
|
||||
|
||||
export const AssemblySymmetryDataParams = {
|
||||
serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL' })
|
||||
serverType: PD.Select('rcsb', [['rcsb', 'RCSB'], ['pdbe', 'PDBe']] as const),
|
||||
serverUrl: PD.Text(AssemblySymmetry.DefaultServerUrl, { description: 'GraphQL endpoint URL (if server type is RCSB) or PDBe API endpoint URL (if server type is PDBe)' })
|
||||
};
|
||||
export type AssemblySymmetryDataParams = typeof AssemblySymmetryDataParams
|
||||
export type AssemblySymmetryDataProps = PD.Values<AssemblySymmetryDataParams>
|
||||
@@ -174,7 +209,7 @@ export const AssemblySymmetryDataProvider: CustomStructureProperty.Provider<Asse
|
||||
|
||||
function getAssemblySymmetryParams(data?: Structure) {
|
||||
return {
|
||||
... AssemblySymmetryDataParams,
|
||||
...AssemblySymmetryDataParams,
|
||||
symmetryIndex: getSymmetrySelectParam(data)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { CollapsableState, CollapsableControls } from '../../../mol-plugin-ui/base';
|
||||
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
|
||||
import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry } from './behavior';
|
||||
import { InitAssemblySymmetry3D, AssemblySymmetry3D, AssemblySymmetryPreset, tryCreateAssemblySymmetry, getRCSBAssemblySymmetryConfig } from './behavior';
|
||||
import { AssemblySymmetryProvider, AssemblySymmetryProps, AssemblySymmetryDataProvider, AssemblySymmetry } from './prop';
|
||||
import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -72,6 +72,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
|
||||
get params() {
|
||||
const structure = this.pivot.cell.obj?.data;
|
||||
const params = PD.clone(structure ? AssemblySymmetryProvider.getParams(structure) : AssemblySymmetryProvider.defaultParams);
|
||||
params.serverType.isHidden = true;
|
||||
params.serverUrl.isHidden = true;
|
||||
return params;
|
||||
}
|
||||
@@ -111,7 +112,9 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
|
||||
}
|
||||
} else {
|
||||
tryCreateAssemblySymmetry(this.plugin, s.cell);
|
||||
await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
|
||||
if (getRCSBAssemblySymmetryConfig(this.plugin).ApplyColors) {
|
||||
await this.plugin.managers.structure.component.updateRepresentationsTheme(components, { color: AssemblySymmetry.Tag.Cluster as any });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -151,5 +154,7 @@ export class AssemblySymmetryControls extends CollapsableControls<{}, AssemblySy
|
||||
const EnableAssemblySymmetry3D = StateAction.build({
|
||||
from: PluginStateObject.Molecule.Structure,
|
||||
})(({ a, ref, state }, plugin: PluginContext) => Task.create('Enable Assembly Symmetry', async ctx => {
|
||||
await AssemblySymmetryPreset.apply(ref, Object.create(null), plugin);
|
||||
const presetParams = AssemblySymmetryPreset.params?.(a, plugin) as PD.Params | undefined;
|
||||
const presetProps = presetParams ? PD.getDefaultValues(presetParams) : Object.create(null);
|
||||
await AssemblySymmetryPreset.apply(ref, presetProps, plugin);
|
||||
}));
|
||||
File diff suppressed because it is too large
Load Diff
@@ -85,7 +85,7 @@ namespace ValidationReport {
|
||||
Clashes = 'rcsb-clashes',
|
||||
}
|
||||
|
||||
export const DefaultBaseUrl = '//ftp.rcsb.org/pub/pdb/validation_reports';
|
||||
export const DefaultBaseUrl = 'https://files.rcsb.org/pub/pdb/validation_reports';
|
||||
export function getEntryUrl(pdbId: string, baseUrl: string) {
|
||||
const id = pdbId.toLowerCase();
|
||||
return `${baseUrl}/${id.substr(1, 2)}/${id}/${id}_validation.xml.gz`;
|
||||
|
||||
30
src/extensions/sb-ncbr/README.md
Normal file
30
src/extensions/sb-ncbr/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# SB NCBR extensions
|
||||
|
||||
## Partial charges
|
||||
|
||||
This extension visualizes partial atomic charges for atoms and residues. The extension reads charge data of a structure from a mmcif file and displays them as a color gradient on the atoms/residues. The coloring uses two gradients: one for positive charges (white-to-blue) and one for negative charges (red-to-white). The color is interpolated between the appropriate gradient based on the charge value. The extension also displays the charge values in the description label when an atom/residue is selected.
|
||||
|
||||
### How to use
|
||||
|
||||
To visualize partial charges, you need to provide a mmcif file with the structure and its charges. The charges are stored under the following categories:
|
||||
|
||||
```
|
||||
_sb_ncbr_partial_atomic_charges_meta.id # id of the charges (e.g. 1)
|
||||
_sb_ncbr_partial_atomic_charges_meta.type # type of the charges (optional, e.g. 'empirical')
|
||||
_sb_ncbr_partial_atomic_charges_meta.method # calculation method name (e.g. 'QEq', 'SQE+qp/Schindler 2021 (PUB_pept)')
|
||||
|
||||
_sb_ncbr_partial_atomic_charges.type_id # id of the charges (pointer to _sb_ncbr_partial_atomic_charges_meta.id)
|
||||
_sb_ncbr_partial_atomic_charges.atom_id # atom id (pointer to _atom_site.id)
|
||||
_sb_ncbr_partial_atomic_charges.charge # partial atomic charge
|
||||
```
|
||||
> Note that the mmcif item `_partial_atomic_charges_meta.method` is used as a description of the charge set in the UI (described in *Controls*).
|
||||
|
||||
The extension will automatically read the charges from the mmcif file and color the structure accordingly.
|
||||
|
||||
### Controls
|
||||
|
||||
The extension provides controls for setting the color gradient range and for selecting charge type (atom charges or residue charges).
|
||||
These controls are available in Color Theme settings for 3D Representation cells in the State Tree UI.
|
||||
|
||||
There is also a dropdown menu for switching between charge sets.
|
||||
These controls are available in Custom Model Properties settings for Model cell in the State Tree UI.
|
||||
3
src/extensions/sb-ncbr/index.ts
Normal file
3
src/extensions/sb-ncbr/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { SbNcbrPartialCharges } from './partial-charges/behavior';
|
||||
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
38
src/extensions/sb-ncbr/partial-charges/behavior.ts
Normal file
38
src/extensions/sb-ncbr/partial-charges/behavior.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { SbNcbrPartialChargesColorThemeProvider } from './color';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { SbNcbrPartialChargesLociLabelProvider } from './labels';
|
||||
import { SbNcbrPartialChargesPreset } from './preset';
|
||||
|
||||
export const SbNcbrPartialCharges = PluginBehavior.create<{ autoAttach: boolean; showToolTip: boolean }>({
|
||||
name: 'sb-ncbr-partial-charges',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'SB NCBR Partial Charges',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean; showToolTip: boolean }> {
|
||||
private SbNcbrPartialChargesLociLabelProvider: LociLabelProvider = SbNcbrPartialChargesLociLabelProvider(
|
||||
this.ctx
|
||||
);
|
||||
|
||||
register(): void {
|
||||
this.ctx.customModelProperties.register(SbNcbrPartialChargesPropertyProvider, this.params.autoAttach);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.add(SbNcbrPartialChargesColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.addProvider(this.SbNcbrPartialChargesLociLabelProvider);
|
||||
this.ctx.builders.structure.representation.registerPreset(SbNcbrPartialChargesPreset);
|
||||
}
|
||||
|
||||
unregister() {
|
||||
this.ctx.customModelProperties.unregister(SbNcbrPartialChargesPropertyProvider.descriptor.name);
|
||||
this.ctx.representation.structure.themes.colorThemeRegistry.remove(SbNcbrPartialChargesColorThemeProvider);
|
||||
this.ctx.managers.lociLabels.removeProvider(this.SbNcbrPartialChargesLociLabelProvider);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(SbNcbrPartialChargesPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
showToolTip: PD.Boolean(true),
|
||||
}),
|
||||
});
|
||||
150
src/extensions/sb-ncbr/partial-charges/color.ts
Normal file
150
src/extensions/sb-ncbr/partial-charges/color.ts
Normal file
@@ -0,0 +1,150 @@
|
||||
import { Bond, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../mol-theme/theme';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Location } from '../../../mol-model/location';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
|
||||
const Colors = {
|
||||
Bond: Color(0xffffff),
|
||||
Error: Color(0x00ff00),
|
||||
MissingCharge: Color(0xffffff),
|
||||
|
||||
Negative: Color(0xff0000),
|
||||
Zero: Color(0xffffff),
|
||||
Positive: Color(0x0000ff),
|
||||
|
||||
getColor: (charge: number, maxCharge: number): Color => {
|
||||
if (charge === 0) return Colors.Zero;
|
||||
if (charge <= -maxCharge) return Colors.Negative;
|
||||
if (charge >= maxCharge) return Colors.Positive;
|
||||
|
||||
const t = maxCharge !== 0 ? Math.abs(charge) / maxCharge : 1;
|
||||
const endColor = charge < 0 ? Colors.Negative : Colors.Positive;
|
||||
|
||||
return Color.interpolate(Colors.Zero, endColor, t);
|
||||
},
|
||||
};
|
||||
|
||||
export const PartialChargesThemeParams = {
|
||||
maxAbsoluteCharge: PD.Numeric(
|
||||
0,
|
||||
{ min: 0 },
|
||||
{
|
||||
label: 'Charge Range',
|
||||
}
|
||||
),
|
||||
absolute: PD.Boolean(false, { isHidden: false, label: 'Use Range' }),
|
||||
chargeType: PD.Select(
|
||||
'residue',
|
||||
[
|
||||
['atom', 'Atom charges'],
|
||||
['residue', 'Residue charges'],
|
||||
],
|
||||
{ isHidden: false }
|
||||
),
|
||||
};
|
||||
export type PartialChargesThemeParams = typeof PartialChargesThemeParams;
|
||||
|
||||
export function getPartialChargesThemeParams() {
|
||||
return PD.clone(PartialChargesThemeParams);
|
||||
}
|
||||
|
||||
export function PartialChargesColorTheme(
|
||||
ctx: ThemeDataContext,
|
||||
props: PD.Values<PartialChargesThemeParams>
|
||||
): ColorTheme<PartialChargesThemeParams> {
|
||||
const model = ctx.structure?.models[0];
|
||||
if (!model) {
|
||||
throw new Error('No model found');
|
||||
}
|
||||
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
|
||||
if (!data) {
|
||||
throw new Error('No partial charges data found');
|
||||
}
|
||||
|
||||
const { absolute, chargeType } = props;
|
||||
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge, maxAbsoluteAtomCharges, maxAbsoluteResidueCharges } = data;
|
||||
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
|
||||
const atomToCharge = typeIdToAtomIdToCharge.get(typeId);
|
||||
const residueToCharge = typeIdToResidueToCharge.get(typeId);
|
||||
|
||||
let maxCharge = 0;
|
||||
if (absolute) {
|
||||
maxCharge = props.maxAbsoluteCharge < 0 ? 0 : props.maxAbsoluteCharge;
|
||||
} else if (chargeType === 'atom') {
|
||||
maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
|
||||
} else {
|
||||
maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
|
||||
}
|
||||
|
||||
// forces coloring updates
|
||||
const contextHash = SbNcbrPartialChargesPropertyProvider.get(model)?.version;
|
||||
|
||||
const chargeMap = chargeType === 'atom' ? atomToCharge : residueToCharge;
|
||||
|
||||
let color: LocationColor;
|
||||
|
||||
if (!chargeMap) {
|
||||
color = (_: Location): Color => Colors.MissingCharge;
|
||||
} else {
|
||||
color = (location: Location): Color => {
|
||||
let id = -1;
|
||||
if (StructureElement.Location.is(location)) {
|
||||
if (Unit.isAtomic(location.unit)) {
|
||||
id = StructureProperties.atom.id(location);
|
||||
}
|
||||
} else if (Bond.isLocation(location)) {
|
||||
if (Unit.isAtomic(location.aUnit)) {
|
||||
const l = StructureElement.Location.create(ctx.structure?.root);
|
||||
l.unit = location.aUnit;
|
||||
l.element = location.aUnit.elements[location.aIndex];
|
||||
id = StructureProperties.atom.id(l);
|
||||
}
|
||||
}
|
||||
|
||||
const charge = chargeMap.get(id);
|
||||
|
||||
if (charge === undefined) {
|
||||
console.warn('No charge found for id', id);
|
||||
return Colors.MissingCharge;
|
||||
}
|
||||
|
||||
return Colors.getColor(charge, maxCharge);
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
factory: PartialChargesColorTheme,
|
||||
granularity: 'group',
|
||||
color,
|
||||
props,
|
||||
description: 'Color atoms and residues based on their partial charge.',
|
||||
preferSmoothing: false,
|
||||
contextHash,
|
||||
};
|
||||
}
|
||||
|
||||
export const SbNcbrPartialChargesColorThemeProvider: ColorTheme.Provider<
|
||||
PartialChargesThemeParams,
|
||||
'sb-ncbr-partial-charges'
|
||||
> = {
|
||||
label: 'SB NCBR Partial Charges',
|
||||
name: 'sb-ncbr-partial-charges',
|
||||
category: ColorTheme.Category.Atom,
|
||||
factory: PartialChargesColorTheme,
|
||||
getParams: getPartialChargesThemeParams,
|
||||
defaultValues: PD.getDefaultValues(PartialChargesThemeParams),
|
||||
isApplicable: (ctx: ThemeDataContext) =>
|
||||
!!ctx.structure &&
|
||||
ctx.structure.models.some((model) => SbNcbrPartialChargesPropertyProvider.isApplicable(model)),
|
||||
ensureCustomProperties: {
|
||||
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) =>
|
||||
data.structure
|
||||
? SbNcbrPartialChargesPropertyProvider.attach(ctx, data.structure.models[0], void 0, true)
|
||||
: Promise.resolve(),
|
||||
detach: (data) => data.structure && SbNcbrPartialChargesPropertyProvider.ref(data.structure.models[0], false),
|
||||
},
|
||||
};
|
||||
40
src/extensions/sb-ncbr/partial-charges/labels.ts
Normal file
40
src/extensions/sb-ncbr/partial-charges/labels.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { StructureElement, StructureProperties } from '../../../mol-model/structure';
|
||||
import { LociLabel } from '../../../mol-plugin-state/manager/loci-label';
|
||||
import { SbNcbrPartialChargesPropertyProvider, hasPartialChargesCategories } from './property';
|
||||
import { Loci } from '../../../mol-model/loci';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { LociLabelProvider } from '../../../mol-plugin-state/manager/loci-label';
|
||||
|
||||
export function SbNcbrPartialChargesLociLabelProvider(ctx: PluginContext): LociLabelProvider {
|
||||
return {
|
||||
label: (loci: Loci) => {
|
||||
if (!StructureElement.Loci.is(loci)) return;
|
||||
|
||||
const model = loci.structure.model;
|
||||
if (!hasPartialChargesCategories(model)) return;
|
||||
const data = SbNcbrPartialChargesPropertyProvider.get(model).value;
|
||||
if (!data) return;
|
||||
|
||||
const loc = StructureElement.Loci.getFirstLocation(loci);
|
||||
if (!loc) return;
|
||||
|
||||
const granularity = ctx.managers.interactivity.props.granularity;
|
||||
if (granularity !== 'element' && granularity !== 'residue') {
|
||||
return;
|
||||
}
|
||||
|
||||
const atomId = StructureProperties.atom.id(loc);
|
||||
const { typeIdToAtomIdToCharge, typeIdToResidueToCharge } = data;
|
||||
|
||||
const typeId = SbNcbrPartialChargesPropertyProvider.props(model).typeId;
|
||||
const showResidueCharge = granularity === 'residue';
|
||||
const charge = showResidueCharge
|
||||
? typeIdToResidueToCharge.get(typeId)?.get(atomId)
|
||||
: typeIdToAtomIdToCharge.get(typeId)?.get(atomId);
|
||||
const label = granularity === 'residue' ? 'Residue charge' : 'Atom charge';
|
||||
|
||||
return `<strong>${label}: ${charge?.toFixed(4) || 'undefined'}</strong>`;
|
||||
},
|
||||
group: (label: LociLabel): string => (label as string).toString().replace(/Model [0-9]+/g, 'Models'),
|
||||
};
|
||||
}
|
||||
32
src/extensions/sb-ncbr/partial-charges/preset.ts
Normal file
32
src/extensions/sb-ncbr/partial-charges/preset.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
PresetStructureRepresentations,
|
||||
StructureRepresentationPresetProvider,
|
||||
} from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { SbNcbrPartialChargesPropertyProvider } from './property';
|
||||
import { SbNcbrPartialChargesColorThemeProvider } from './color';
|
||||
|
||||
export const SbNcbrPartialChargesPreset = StructureRepresentationPresetProvider({
|
||||
id: 'sb-ncbr-partial-charges-preset',
|
||||
display: {
|
||||
name: 'SB NCBR Partial Charges',
|
||||
group: 'Annotation',
|
||||
description: 'Color atoms and residues based on their partial charge.',
|
||||
},
|
||||
isApplicable(a) {
|
||||
return !!a.data.models.some((m) => SbNcbrPartialChargesPropertyProvider.isApplicable(m));
|
||||
},
|
||||
params: () => StructureRepresentationPresetProvider.CommonParams,
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const colorTheme = SbNcbrPartialChargesColorThemeProvider.name as any;
|
||||
return PresetStructureRepresentations.auto.apply(
|
||||
ref,
|
||||
{ ...params, theme: { globalName: colorTheme, focus: { name: colorTheme, params: { chargeType: 'atom' } } } },
|
||||
plugin
|
||||
);
|
||||
},
|
||||
});
|
||||
204
src/extensions/sb-ncbr/partial-charges/property.ts
Normal file
204
src/extensions/sb-ncbr/partial-charges/property.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { Model } from '../../../mol-model/structure';
|
||||
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
|
||||
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
|
||||
import { arrayMinMax } from '../../../mol-util/array';
|
||||
|
||||
type TypeId = number;
|
||||
type IdToCharge = Map<number, number>;
|
||||
export interface SBNcbrPartialChargeData {
|
||||
typeIdToMethod: Map<TypeId, string>;
|
||||
typeIdToAtomIdToCharge: Map<TypeId, IdToCharge>;
|
||||
typeIdToResidueToCharge: Map<TypeId, IdToCharge>;
|
||||
maxAbsoluteAtomCharges: IdToCharge;
|
||||
maxAbsoluteResidueCharges: IdToCharge;
|
||||
maxAbsoluteAtomChargeAll: number;
|
||||
params: PartialChargesPropertyParams;
|
||||
}
|
||||
|
||||
const PartialChargesPropertyParams = {
|
||||
typeId: PD.Select<number>(0, [[0, '0']]),
|
||||
};
|
||||
type PartialChargesPropertyParams = typeof PartialChargesPropertyParams;
|
||||
const DefaultPartialChargesPropertyParams = PD.clone(PartialChargesPropertyParams);
|
||||
|
||||
function getParams(model: Model) {
|
||||
return getData(model).value?.params ?? DefaultPartialChargesPropertyParams;
|
||||
}
|
||||
|
||||
const PropertyKey = 'sb-ncbr-partial-charges-property-data';
|
||||
|
||||
function getData(model: Model): CustomProperty.Data<SBNcbrPartialChargeData | undefined> {
|
||||
if (PropertyKey in model._staticPropertyData) {
|
||||
return model._staticPropertyData[PropertyKey];
|
||||
}
|
||||
|
||||
let data: CustomProperty.Data<SBNcbrPartialChargeData | undefined>;
|
||||
|
||||
if (!SbNcbrPartialChargesPropertyProvider.isApplicable(model)) {
|
||||
data = { value: undefined };
|
||||
} else {
|
||||
const typeIdToMethod = getTypeIdToMethod(model);
|
||||
const typeIdToAtomIdToCharge = getTypeIdToAtomIdToCharge(model);
|
||||
const typeIdToResidueToCharge = getTypeIdToResidueIdToCharge(model, typeIdToAtomIdToCharge);
|
||||
const maxAbsoluteAtomCharges = getMaxAbsoluteCharges(typeIdToAtomIdToCharge);
|
||||
const maxAbsoluteResidueCharges = getMaxAbsoluteCharges(typeIdToResidueToCharge);
|
||||
const maxAbsoluteAtomChargeAll = getMaxAbsoluteAtomChargeAll(maxAbsoluteAtomCharges, maxAbsoluteResidueCharges);
|
||||
|
||||
const options = Array.from(typeIdToMethod.entries()).map(
|
||||
([typeId, method]) => [typeId, method] as [number, string]
|
||||
);
|
||||
const params = {
|
||||
typeId: PD.Select<number>(1, options),
|
||||
};
|
||||
|
||||
data = {
|
||||
value: {
|
||||
typeIdToMethod,
|
||||
typeIdToAtomIdToCharge,
|
||||
typeIdToResidueToCharge,
|
||||
maxAbsoluteAtomCharges,
|
||||
maxAbsoluteResidueCharges,
|
||||
maxAbsoluteAtomChargeAll,
|
||||
params,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
model._staticPropertyData[PropertyKey] = data;
|
||||
return data;
|
||||
}
|
||||
|
||||
function getTypeIdToMethod(model: Model) {
|
||||
const typeIdToMethod: SBNcbrPartialChargeData['typeIdToMethod'] = new Map();
|
||||
|
||||
const sourceData = model.sourceData as MmcifFormat;
|
||||
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.rowCount;
|
||||
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('id');
|
||||
const methods = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges_meta.getField('method');
|
||||
|
||||
if (!typeIds || !methods) {
|
||||
return typeIdToMethod;
|
||||
}
|
||||
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const typeId = typeIds.int(i);
|
||||
const method = methods.str(i);
|
||||
typeIdToMethod.set(typeId, method);
|
||||
}
|
||||
|
||||
return typeIdToMethod;
|
||||
}
|
||||
|
||||
function getTypeIdToAtomIdToCharge(model: Model): SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] {
|
||||
const atomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge'] = new Map();
|
||||
|
||||
const sourceData = model.sourceData as MmcifFormat;
|
||||
const rowCount = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.rowCount;
|
||||
const typeIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('type_id');
|
||||
const atomIds = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('atom_id');
|
||||
const charges = sourceData.data.frame.categories.sb_ncbr_partial_atomic_charges.getField('charge');
|
||||
|
||||
if (!typeIds || !atomIds || !charges) return atomIdToCharge;
|
||||
|
||||
for (let i = 0; i < rowCount; ++i) {
|
||||
const typeId = typeIds.int(i);
|
||||
const atomId = atomIds.int(i);
|
||||
const charge = charges.float(i);
|
||||
if (!atomIdToCharge.has(typeId)) atomIdToCharge.set(typeId, new Map());
|
||||
atomIdToCharge.get(typeId)?.set(atomId, charge);
|
||||
}
|
||||
|
||||
return atomIdToCharge;
|
||||
}
|
||||
|
||||
function getTypeIdToResidueIdToCharge(
|
||||
model: Model,
|
||||
typeIdToAtomIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
|
||||
) {
|
||||
const { offsets, count } = model.atomicHierarchy.residueAtomSegments;
|
||||
const { atomId: atomIds } = model.atomicConformation;
|
||||
|
||||
const residueToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge'] = new Map();
|
||||
|
||||
typeIdToAtomIdToCharge.forEach((atomIdToCharge, typeId: number) => {
|
||||
if (!residueToCharge.has(typeId)) residueToCharge.set(typeId, new Map());
|
||||
const residueCharges = residueToCharge.get(typeId)!;
|
||||
for (let rI = 0; rI < count; rI++) {
|
||||
let charge = 0;
|
||||
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
|
||||
const atom_id = atomIds.value(aI);
|
||||
charge += atomIdToCharge.get(atom_id) || 0;
|
||||
}
|
||||
for (let aI = offsets[rI], _aI = offsets[rI + 1]; aI < _aI; aI++) {
|
||||
const atom_id = atomIds.value(aI);
|
||||
residueCharges.set(atom_id, charge);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return residueToCharge;
|
||||
}
|
||||
|
||||
function getMaxAbsoluteCharges(
|
||||
typeIdToCharge: SBNcbrPartialChargeData['typeIdToAtomIdToCharge']
|
||||
): SBNcbrPartialChargeData['maxAbsoluteAtomCharges'];
|
||||
function getMaxAbsoluteCharges(
|
||||
typeIdToCharge: SBNcbrPartialChargeData['typeIdToResidueToCharge']
|
||||
): SBNcbrPartialChargeData['maxAbsoluteResidueCharges'] {
|
||||
const maxAbsoluteCharges: Map<number, number> = new Map();
|
||||
|
||||
typeIdToCharge.forEach((idToCharge, typeId) => {
|
||||
const charges = Array.from(idToCharge.values());
|
||||
const [min, max] = arrayMinMax(charges);
|
||||
const bound = Math.max(Math.abs(min), max);
|
||||
maxAbsoluteCharges.set(typeId, bound);
|
||||
});
|
||||
|
||||
return maxAbsoluteCharges;
|
||||
}
|
||||
|
||||
function getMaxAbsoluteAtomChargeAll(
|
||||
maxAbsoluteAtomCharges: SBNcbrPartialChargeData['maxAbsoluteAtomCharges'],
|
||||
maxAbsoluteResidueCharges: SBNcbrPartialChargeData['maxAbsoluteResidueCharges']
|
||||
): number {
|
||||
let maxAbsoluteCharge = 0;
|
||||
|
||||
maxAbsoluteAtomCharges.forEach((_, typeId) => {
|
||||
const maxCharge = maxAbsoluteAtomCharges.get(typeId) || 0;
|
||||
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
|
||||
});
|
||||
maxAbsoluteResidueCharges.forEach((_, typeId) => {
|
||||
const maxCharge = maxAbsoluteResidueCharges.get(typeId) || 0;
|
||||
if (maxCharge > maxAbsoluteCharge) maxAbsoluteCharge = maxCharge;
|
||||
});
|
||||
|
||||
return maxAbsoluteCharge;
|
||||
}
|
||||
|
||||
export function hasPartialChargesCategories(model: Model): boolean {
|
||||
if (!model || !MmcifFormat.is(model.sourceData)) return false;
|
||||
const { categories } = model.sourceData.data.frame;
|
||||
return (
|
||||
'atom_site' in categories &&
|
||||
'sb_ncbr_partial_atomic_charges' in categories &&
|
||||
'sb_ncbr_partial_atomic_charges_meta' in categories
|
||||
);
|
||||
}
|
||||
|
||||
export const SbNcbrPartialChargesPropertyProvider: CustomModelProperty.Provider<
|
||||
PartialChargesPropertyParams,
|
||||
SBNcbrPartialChargeData | undefined
|
||||
> = CustomModelProperty.createProvider({
|
||||
label: 'SB NCBR Partial Charges Property Provider',
|
||||
descriptor: CustomPropertyDescriptor({
|
||||
name: 'sb-ncbr-partial-charges-property-provider',
|
||||
}),
|
||||
type: 'static',
|
||||
defaultParams: DefaultPartialChargesPropertyParams,
|
||||
getParams: (data: Model) => getParams(data),
|
||||
isApplicable: (model: Model) => hasPartialChargesCategories(model),
|
||||
obtain: (_ctx: CustomProperty.Context, model: Model) => Promise.resolve(getData(model)),
|
||||
});
|
||||
@@ -17,6 +17,7 @@ import { PluginCommands } from '../../mol-plugin/commands';
|
||||
import { PluginContext } from '../../mol-plugin/context';
|
||||
import { StateObjectCell, StateSelection, StateTransform } from '../../mol-state';
|
||||
import { shallowEqualObjects } from '../../mol-util';
|
||||
import { Choice } from '../../mol-util/param-choice';
|
||||
import { ParamDefinition } from '../../mol-util/param-definition';
|
||||
import { MeshlistData } from '../meshes/mesh-extension';
|
||||
|
||||
@@ -30,7 +31,7 @@ import { VolsegState, VolsegStateData, VolsegStateParams } from './entry-state';
|
||||
import { VolsegVolumeData, SimpleVolumeParamValues, VOLUME_VISUAL_TAG } from './entry-volume';
|
||||
import * as ExternalAPIs from './external-api';
|
||||
import { VolsegGlobalStateData } from './global-state';
|
||||
import { applyEllipsis, Choice, isDefined, lazyGetter, splitEntryId } from './helpers';
|
||||
import { applyEllipsis, isDefined, lazyGetter, splitEntryId } from './helpers';
|
||||
import { type VolsegStateFromEntry } from './transformers';
|
||||
import { StateTransforms } from '../../mol-plugin-state/transforms';
|
||||
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../mol-plugin-state/objects';
|
||||
import { Choice } from '../../mol-util/param-choice';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
|
||||
import { Choice } from './helpers';
|
||||
|
||||
|
||||
export const VolumeTypeChoice = new Choice({ 'isosurface': 'Isosurface', 'direct-volume': 'Direct volume', 'off': 'Off' }, 'isosurface');
|
||||
export type VolumeType = Choice.Values<typeof VolumeTypeChoice>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user