Compare commits

..

452 Commits

Author SHA1 Message Date
Alexander Rose
6c1d17bac5 3.36.1 2023-06-11 12:44:43 -07:00
Alexander Rose
ad2ccf4e07 fix changelog 2023-06-11 12:41:59 -07:00
Alexander Rose
dc1b7b4693 3.36.0 2023-06-11 12:28:28 -07:00
Alexander Rose
59e4e2b31d schema updates 2023-06-11 12:25:05 -07:00
Alexander Rose
d2483dc449 package updates 2023-06-11 12:23:42 -07:00
Alexander Rose
d26946e9ee Merge pull request #838 from molstar/cartoon-color-nucleic
Cartoon improvements
2023-06-05 23:06:06 -07:00
Alexander Rose
cd045a6b48 fix spec 2023-06-05 22:49:43 -07:00
Alexander Rose
2407729d27 simplify sub color theme creation 2023-06-05 22:43:51 -07:00
Alexander Rose
1aa22b9fa0 add saturation/lightness to uniform color theme 2023-06-05 22:42:34 -07:00
Alexander Rose
35c9f39a69 add cartoon color theme
- separates colorings for for mainchain and sidechain visuals
- uses isSecondary mechanism of LocationIterator
2023-06-03 11:15:33 -07:00
Alexander Rose
7dd420cc18 add nucleicProfile param to cartoon repr 2023-06-03 11:13:08 -07:00
Alexander Rose
1d434c259a ignore LociLabelManager without providers 2023-05-29 15:01:26 -07:00
Alexander Rose
6d193edd68 add keyReleased event 2023-05-29 15:01:14 -07:00
Alexander Rose
9bf859d6ed Merge pull request #835 from JonStargaryen/channels
Adjust VolumeRepresentation#setState
2023-05-29 11:41:33 -07:00
Sebastian Bittrich
207230d565 check against _state 2023-05-29 11:16:17 -07:00
Alexander Rose
b7a673f38e Merge pull request #731 from JonStargaryen/master
Parse CCD Files
2023-05-29 11:11:57 -07:00
Sebastian Bittrich
2204e4e0d0 merge upstream 2023-05-29 09:24:11 -07:00
Sebastian Bittrich
6276365766 update setState of unit repr 2023-05-29 09:18:07 -07:00
Sebastian Bittrich
505b04c92d merge upstream 2023-05-29 08:56:37 -07:00
Alexander Rose
fc84dcb037 Merge pull request #836 from molstar/structure-selection-snapshot
add snapshot support for structure selections
2023-05-28 12:32:37 -07:00
Alexander Rose
2f29ff7314 Merge pull request #826 from molstar/graph-bonds
Graph & bonds calc tweaks & fixes
2023-05-27 11:07:56 -07:00
Alexander Rose
b37f043876 Merge branch 'master' into graph-bonds 2023-05-27 11:07:45 -07:00
Alexander Rose
f0e725f65c tweak CCD coordinate type handling 2023-05-27 11:01:27 -07:00
Alexander Rose
23a34e2df1 add snapshot support for structure selections 2023-05-27 10:14:46 -07:00
Alexander Rose
d11e242b70 fix background occlusion handling 2023-05-27 10:07:58 -07:00
Sebastian Bittrich
d9af0ca068 cl 2023-05-26 14:04:31 -07:00
Sebastian Bittrich
b7f10acbf0 adjust VolumeRepresentation#setState - closes #210 2023-05-26 13:59:45 -07:00
Sebastian Bittrich
43749ccdbd merge 2023-05-23 09:24:46 -07:00
David Sehnal
3bf4a8f8e6 optimize computeInterUnitBonds (#830) 2023-05-23 09:08:00 +02:00
Alexander Rose
f0ae1b3347 fix EdgeBuilder.addNextEdge for loop edges 2023-05-22 23:27:06 -07:00
Alexander Rose
99809d25b9 remove erroneous bounding-box overlap test 2023-05-22 23:25:08 -07:00
Sebastian Bittrich
e83c0af67c meta 2023-05-22 13:10:47 -07:00
Sebastian Bittrich
2ddf94313e Merge remote-tracking branch 'upstream/master' 2023-05-22 13:08:41 -07:00
Sebastian Bittrich
da5965c956 meta 2023-05-22 13:04:58 -07:00
Sebastian Bittrich
31be0af3c9 shrink diff 2023-05-22 12:55:23 -07:00
Sebastian Bittrich
38c550b245 factor out to extension 2023-05-22 11:59:57 -07:00
Sebastian Bittrich
95a7a2cef9 rm CCDCoordinateTypeProp 2023-05-22 09:16:28 -07:00
Alexander Rose
1a1ec51736 fix bbox overlap test in Structure.eachUnitPair 2023-05-20 22:46:59 -07:00
Alexander Rose
299aae56c1 typing improvements 2023-05-20 19:22:04 -07:00
Alexander Rose
781824c961 operators in IndexPairBonds as directed property 2023-05-20 19:21:40 -07:00
Sebastian Bittrich
930cfa2590 comment 2023-05-15 11:45:39 -07:00
Sebastian Bittrich
35439f01aa rather heavy-handed tracking of ideal/model coords 2023-05-15 11:44:04 -07:00
Alexander Rose
71121e52af 3.35.0 2023-05-14 11:50:42 -07:00
Alexander Rose
c08155717f changelog 2023-05-14 11:47:29 -07:00
Alexander Rose
de4164e7a4 package updates 2023-05-14 11:47:04 -07:00
Alexander Rose
e34d1242a9 Merge pull request #820 from molstar/cantor-mol-script
add cantor-pairing functions to mol-script
2023-05-14 11:40:18 -07:00
Alexander Rose
5b82641018 add sign to core.math in mol-script 2023-05-13 18:22:21 -07:00
Alexander Rose
de2f0c27b2 add trunc to core.math in mol-script 2023-05-13 18:08:19 -07:00
Alexander Rose
71e2afe781 tweak principal-axes spec 2023-05-13 16:30:23 -07:00
Alexander Rose
3cba621fcf add cantor-pairing functions to mol-script 2023-05-13 16:22:50 -07:00
Alexander Rose
d79a2077c1 package updates 2023-05-13 12:37:21 -07:00
Alexander Rose
6925547b5f Merge pull request #804 from giagitom/single-aromatic-dash-count
Enable odd dash count (1,3,5)
2023-05-13 11:41:12 -07:00
Alexander Rose
84aae8cf0a Merge branch 'master' into single-aromatic-dash-count 2023-05-13 11:17:47 -07:00
Alexander Rose
bdb42e39ec Merge pull request #809 from giagitom/principal-axis-spec
Adding principal axes spec
2023-05-13 10:23:29 -07:00
Alexander Rose
6edd54ee6d use Vec3.isFinite as name 2023-05-13 10:12:47 -07:00
Alexander Rose
198f884d8b Merge pull request #817 from molstar/pdb-assemblies-fix
Fix Archive PDB assembly loading
2023-05-13 10:04:34 -07:00
Alexander Rose
2c7d0a6721 comment 2023-05-13 10:03:39 -07:00
Alexander Rose
7ef15ede0d mesh exporter improvements
- set alphaMode and doubleSided in glTF export
- fix flipped cylinder caps
2023-05-13 09:45:27 -07:00
Sebastian Bittrich
3d96298b55 Merge remote-tracking branch 'upstream/master' 2023-05-12 14:45:40 -07:00
Sebastian Bittrich
964f045e56 refactor 2023-05-12 11:52:18 -07:00
Sebastian Bittrich
d3364ac109 control what coord set to show 2023-05-12 11:28:51 -07:00
Sebastian Bittrich
a5b963c919 hide hydrogens by default 2023-05-12 09:41:56 -07:00
Gianluca Tomasello
d6fcbbf543 Merge branch 'master' into single-aromatic-dash-count 2023-05-12 15:16:02 +02:00
giagitom
f2e7e2eaf2 Fix unequeal aromatic dashes 2023-05-12 15:13:06 +02:00
giagitom
01c4c63114 Merge brunch master 2023-05-12 15:10:50 +02:00
Sebastian Bittrich
22f9bc4ff1 swap ideal/model labels, handle entirely missing coords 2023-05-11 15:42:13 -07:00
Sebastian Bittrich
c6c4350638 align differing atom sets 2023-05-11 11:28:40 -07:00
giagitom
a17a0c4527 Added changelog record 2023-05-11 16:37:25 +02:00
giagitom
8e507012c1 Merge branch 'master' into principal-axis-spec 2023-05-11 16:14:20 +02:00
giagitom
beb4351dc9 Add check to avoid non-finite origin vector 2023-05-11 16:13:16 +02:00
dsehnal
afbb940721 headers 2023-05-11 13:24:17 +02:00
dsehnal
f5c619a4c7 Fix Archive PDB assembly loading 2023-05-11 13:23:28 +02:00
Sebastian Bittrich
1b0401dff5 filter for present valuekind 2023-05-09 14:50:37 -07:00
valasatava
649e779100 Add HYP to the list of amino acids (#815)
* add modified amino acid "hydroxyproline" (HYP) present in collagen molecules to the list of amino acids

* update changelog
2023-05-09 16:27:42 +02:00
Alexander Rose
f61e0e72a8 move web3dsurvey analytics into iframe
- only deployed on molstar.org
2023-05-07 20:41:29 -07:00
Alexander Rose
803f75fdde Merge pull request #797 from molstar/ignore-hydrogens-interactions
support ignoreHydrogens for interactions
2023-05-07 19:56:33 -07:00
Alexander Rose
718d314eda Merge branch 'master' into ignore-hydrogens-interactions 2023-05-07 19:56:19 -07:00
Alexander Rose
adab6b0a6a Merge pull request #812 from valasatava/pdb-to-cif-header
Parse HEADER record when reading PDB file
2023-05-06 16:49:36 -07:00
Alexander Rose
d295ed2eca add options to mesh-exporter
- lines/points as triangles
- include hidden
- primitives qaulity
2023-05-06 16:35:01 -07:00
Yana Rose
d18cbfa8cf update the date in the added file header 2023-05-05 12:33:39 -07:00
Yana Rose
59f881c4be handle empty HEADER record 2023-05-05 12:32:39 -07:00
JonStargaryen
0295e0ef63 factor out CoordinateType 2023-05-05 09:41:46 -07:00
Yana Rose
dcdb95a055 update changelog 2023-05-04 22:11:36 -07:00
Yana Rose
e379d27722 add my name to package.json's contributors 2023-05-04 22:11:22 -07:00
Yana Rose
41fbe0d2b7 update headers of modified files 2023-05-04 22:11:05 -07:00
Yana Rose
1231666b06 restore package-lock 2023-05-04 21:52:56 -07:00
Yana Rose
b302bb8455 parse header when reading PDB file 2023-05-04 21:30:02 -07:00
Sebastian Bittrich
6e82405600 fix merge 2023-05-04 16:25:52 -07:00
Sebastian Bittrich
a678893bdb wip 2023-05-04 16:20:28 -07:00
Alexander Rose
430348a3cd support points & lines in glTF export (#810) 2023-05-03 19:23:40 +02:00
midlik
315401c166 struct-conn extension (#802)
* struct-conn extension toy example

* Minor changes (David's feedback)

* Showing struct_conn visuals

* Removed Interactions visual

* Caching struct_conns

* Removed testing buttons in index.html, updated CHANGELOG

* Addressed most of PR feedback

* Fixed structure node selection, docs

* Addressed feedback round 2
2023-05-03 18:01:51 +02:00
giagitom
b309c545f5 - Fixed dashes spacing
- Added changelog entry
2023-05-03 17:51:02 +02:00
David Sehnal
60b5d2d39b fix labels & optimize getData (#811) 2023-05-03 15:37:24 +02:00
David Sehnal
eb749a2a16 State.updateNode fix for Null parents (#807) 2023-05-02 16:12:38 +02:00
Dominik Tichy
6db96001a3 Partial atomic charges extension (#808)
* feat: partial charges extension

* chore: pullrequest actions

* feat: example

* fix: review changes

* fix: updated example structure categories

* fix(changelog): shorter description

* fix: code review changes

* fix: cosmetic changes
2023-05-02 16:09:42 +02:00
giagitom
257370ad58 - Removing unused variables
- Supporting meshes and lines
- Remove non-stub cap from last (if odd) dash
2023-05-01 22:02:33 +02:00
Alexander Rose
557bf63b55 prevent dragging of snapshot images 2023-04-30 21:10:24 -07:00
giagitom
0e32e0a785 lint-fix 2023-04-30 16:22:30 +02:00
giagitom
f2c607a4b2 Adding principal axes spec 2023-04-30 16:03:07 +02:00
David Sehnal
0d12a9e118 PluginState.setSnapshot fix (#805) 2023-04-27 12:34:44 +02:00
giagitom
02f418c8c5 Fix 2023-04-26 15:31:24 +02:00
giagitom
ba618c9e4a Allow for single aromatic dash count 2023-04-26 14:56:42 +02:00
David Sehnal
ebd3ebe7b2 fix struct conn for waters (#803) 2023-04-25 16:25:39 +02:00
Alexander Rose
804117475b lint fixes 2023-04-22 10:18:59 -07:00
Alexander Rose
09ab8d6219 Merge pull request #796 from MadCatX/ntc-tube-uniform-color
Add a uniform color theme for NtC tube that still paints the residue and segment dividers in a different color
2023-04-22 10:18:05 -07:00
Alexander Rose
f3a5369690 support ignoreHydrogens for interactions 2023-04-22 09:56:21 -07:00
Michal Malý
8bf2fe624d Add a uniform color theme for NtC tube that still paints the residue and
segment dividers in a different color
2023-04-20 17:38:55 +02:00
Alexander Rose
50c1b667c5 3.34.0 2023-04-16 12:19:38 -07:00
Alexander Rose
360031d37c Merge branch 'master' of https://github.com/molstar/molstar 2023-04-16 12:09:47 -07:00
Alexander Rose
9ec873e0db changelog 2023-04-16 11:58:31 -07:00
Alexander Rose
c830a720b0 package updates 2023-04-16 11:56:32 -07:00
Alexander Rose
1aa7d1e0f7 Merge pull request #782 from molstar/eachLocation
add eachLocation to representation/visual interface
2023-04-16 11:53:44 -07:00
Alexander Rose
c5c8de8628 Merge branch 'master' of https://github.com/molstar/molstar into eachLocation 2023-04-16 11:44:45 -07:00
Alexander Rose
74c6d6f5a1 changelog 2023-04-16 11:44:03 -07:00
Russell Parker
2bff0faff7 Address Node incompatibility in mol-util/file-info (#787)
* Alter getFileInfo to avoid `instanceof WebAPIClass` and simple renames

* Remove note

* Fix potentially bad `window` reference
2023-04-13 23:41:21 +02:00
dsehnal
4df028aa77 readAllLinesAsync fix 2023-04-13 20:33:05 +02:00
dsehnal
47c2d153aa tweak readLinesAsync 2023-04-13 20:25:20 +02:00
Alexander Rose
18be09e9d5 fix .getAllLoci for representations with structure.child 2023-04-12 23:28:16 -07:00
Alexander Rose
55e940e88c fix rendering with very small viewport and SSAO 2023-04-12 23:25:57 -07:00
Alexander Rose
e246f4e5ca add eachLocation to representation/visual interface 2023-04-08 10:17:00 -07:00
Alexander Rose
5e1bb4b106 Merge pull request #778 from MadCatX/ntc-tube-missing-atoms
Fix rendering of NtC tube when some of the required atoms are missing
2023-04-08 09:55:51 -07:00
Alexander Rose
0b2889bb99 Merge branch 'master' into ntc-tube-missing-atoms 2023-04-08 09:55:44 -07:00
Alexander Rose
2994caf411 Merge pull request #779 from MadCatX/restore-vertex-array-per-program
Fix broken rendering caused by changes in 291d7abb78
2023-04-08 09:53:55 -07:00
Alexander Rose
e157993a0f Merge branch 'master' of https://github.com/molstar/molstar into pr/MadCatX/779 2023-04-08 09:42:53 -07:00
Alexander Rose
6c7c9afc34 fix spec 2023-04-08 09:40:10 -07:00
Alexander Rose
2d0b17d93c improve trackball keyState handling with modifiers 2023-04-08 09:29:57 -07:00
midlik
033c613c89 Added "Zoom All", "Orient Axes", "Reset Axes" buttons (#776)
* Added "Zoom All", "Orient Axes", "Reset Axes" buttons

* Addressed PR776 feedback
2023-04-08 10:06:39 +02:00
Michal Malý
1985eb59dd Do not reuse vertex arrays among programs 2023-04-06 15:00:54 +02:00
Michal Malý
1cf6cbf8a3 changelog 2023-04-06 12:32:21 +02:00
Michal Malý
0b42379c34 Do not draw a NtC tube segment unless we have all required atoms 2023-04-06 12:20:13 +02:00
Alexander Rose
414c349974 changelog 2023-04-03 21:56:59 -07:00
Alexander Rose
cf6d5f7194 Merge pull request #774 from giagitom/markingDepth-avoid-alpha-0-rendering
Marking depth avoid alpha 0 rendering
2023-04-03 21:54:18 -07:00
Alexander Rose
949f5207b4 add ModifiersKeys.areNone 2023-04-03 21:46:28 -07:00
Alexander Rose
a1da374b32 add ModifiersKeys.areNone 2023-04-03 21:44:11 -07:00
giagitom
5460322d4a Update changelog 2023-04-03 12:50:27 +02:00
giagitom
8b2da0b787 avoid rendering of alpha 0 renderables on renderMarkingDepth 2023-04-03 12:46:11 +02:00
Alexander Rose
3eaf4dacaf 3.33.0 2023-04-02 12:48:10 -07:00
Alexander Rose
d66d9b4dd7 changelog 2023-04-02 12:46:11 -07:00
Alexander Rose
cc52279e01 package updates 2023-04-02 12:45:42 -07:00
Alexander Rose
0def474f6d Merge pull request #773 from molstar/model-conf-fields
include occupancy & B_iso_or_equiv in model conformation
2023-04-02 12:31:34 -07:00
Alexander Rose
e0ea9a2855 Merge branch 'master' into model-conf-fields 2023-04-02 12:31:21 -07:00
midlik
2bc381fe05 Forsake lazy imports (#772)
* Removed LazyImports (gl, pngjs, jpeg-js required as param of HeadlessPluginConstructor)

* Added a few methods to HeadlessPluginContext for rendering image without saving to file

* Updated CHANGELOG

* Lint

* Rolled back removing @types/jpeg-js from deps
2023-04-02 18:47:22 +02:00
Alexander Rose
fb3cd3bf52 include occupancy & B_iso_or_equiv in model conformation 2023-04-01 19:49:24 -07:00
Alexander Rose
c4414c7cc4 Merge pull request #581 from molstar/mmcif/parse-all-blocks
add TrajectoryFromMmCif loadAllBlocks param
2023-04-01 16:25:56 -07:00
Alexander Rose
e2f2ceb7a9 Merge branch 'master' into mmcif/parse-all-blocks 2023-04-01 16:25:45 -07:00
Alexander Rose
641e7efb11 improve camera when toggeling pointer-lock/fly-mode 2023-04-01 16:13:31 -07:00
Alexander Rose
11f2ef50ef add Frustum3D and Plane3D math primitives 2023-04-01 11:43:52 -07:00
Alexander Rose
869ecfaf71 improve typing of toArray methods 2023-04-01 11:33:01 -07:00
Alexander Rose
cb8731815c changelog & param tweaks 2023-04-01 11:02:06 -07:00
Alexander Rose
a9177ad362 Merge branch 'master' of https://github.com/molstar/molstar into mmcif/parse-all-blocks 2023-04-01 10:44:49 -07:00
Alexander Rose
ad116df73b fix camera project/unproject
- was wrong when using offset viewport
2023-03-31 23:37:52 -07:00
Alexander Rose
f30b3a410c init camera for fly mode
- like for pointer-lock
2023-03-30 22:32:58 -07:00
Alexander Rose
c440ba2d4b gl tweaks
- add more docs
- clean schema types
2023-03-30 22:16:11 -07:00
Alexander Rose
a3267dafdb Merge pull request #762 from molstar/multi-scale-ssao
add multi-scale ssao
2023-03-30 22:13:43 -07:00
dsehnal
7a1e83733c throttle canvas resize events 2023-03-30 15:00:24 +02:00
Russell Parker
7cb96ce983 Handle resizing viewer element when window remains the same size (#763)
* Handle resizing viewer element when window remains the same size

* Fix bad rebase

* Fall back to window resize event listener when ResizeObserver not defined

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-03-30 14:51:20 +02:00
jpattle
a73633d0c3 Selection and focus improvements (#742)
* Updated measurements so that the toggle selection button is not shown if selection mode is turned off

* Updated selection controls so that they cannot be turned off if selection mode is not shown

* Extended camera focus bindings to allow override of behaviour to reset camera focus on click

* Exported default bindings for selection and focus so that they can be more easily selectively overridden

* Added description to changelog and headers to modified files

* Fixed spacing in text for toggling selection mode

* Updated camera bindings to not be a breaking change by setting the new bindings to be optional and using default values when undefined

* resolved linting issues with camera bindings

* updated superposition UI to hide selection toggle button when selection mode is disabled

* updated the default value for click to reset camera bindings
2023-03-30 14:49:35 +02:00
Alexander Rose
b2f8e8dd4e fix spec 2023-03-30 00:07:25 -07:00
Alexander Rose
291d7abb78 gl improvements
- remove unneccesary return values
- reuse vertex array among programs
- typo fixes
- cleanup material-textures
2023-03-29 23:50:30 -07:00
Alexander Rose
32873d787b fix theme smoothing not updated 2023-03-29 23:22:55 -07:00
Alexander Rose
e243d71abf cleanup level, light, clip assignments 2023-03-29 23:19:27 -07:00
Alexander Rose
2689d3f21a more input/controls fixes & tweaks
- no identify when pointer-lock & controls movement
- limit controls key bindings to viewport
- take controls minDistance into account for movement
2023-03-28 23:22:55 -07:00
dsehnal
c1bc008114 Fix JSX reference 2023-03-28 10:04:36 +02:00
Alexander Rose
254578460a input/controls fixes & tweaks
- assign trackball bindings in setProps
- remove cross element in input-observer dispose
- improve key event target handling
- add center dot to pointer-lock crosshair
2023-03-27 23:09:57 -07:00
Alexander Rose
f5467dd3b9 allow intra-residue contacts in single-residue models 2023-03-27 22:39:32 -07:00
Alexander Rose
9eb8714e11 add multi-scale ssao 2023-03-26 00:14:49 -07:00
Alexander Rose
847678ea56 improve canvas3d consoleStats 2023-03-26 00:14:05 -07:00
Alexander Rose
f08729a402 apply bumpiness as lightness with ignoreLight 2023-03-25 23:57:18 -07:00
Alexander Rose
a7c91257a7 Merge pull request #752 from molstar/input-controls
input/controls improvements
2023-03-25 23:52:10 -07:00
Alexander Rose
835369a91e change dragRotateZ binding to drag left+shift+ctrl 2023-03-25 10:50:38 -07:00
Alexander Rose
62554b522f add key bindings for fly mode & reset view 2023-03-25 10:32:02 -07:00
Alexander Rose
fd041cd4c3 change dragRotateZ binding to left+alt 2023-03-25 10:31:05 -07:00
Alexander Rose
cfbb68c8ef improve contrast of pointer-lock cross 2023-03-25 10:30:05 -07:00
dsehnal
d7acec4f7d tweak moveCamera 2023-03-25 11:58:06 +01:00
dsehnal
7da46bca8b scale move speed by frametime 2023-03-24 19:52:11 +01:00
Sebastian Bittrich
66b4fcdc2c simplify 2023-03-23 08:48:58 -07:00
Alexander Rose
c480579ca8 add web3dsurvey analytics code (only molstar.org) 2023-03-22 20:13:01 -07:00
Alexander Rose
00ff1a1eae Merge branch 'master' of https://github.com/molstar/molstar into input-controls 2023-03-21 20:19:33 -07:00
dsehnal
ae795f8ad3 3.32.0 2023-03-20 09:29:41 +01:00
dsehnal
9d3c071689 changelog 2023-03-20 09:26:48 +01:00
David Sehnal
01cb23f566 add setFSModule (#755) 2023-03-20 09:24:45 +01:00
Alexander Rose
fe8a9799ab add exposure parameter (#751)
* add exposure parameter

* add missing uniform

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-03-20 09:16:51 +01:00
David Sehnal
4f18154681 Marking improvements (#750)
* better marking identify

* changelog

* tweak

* type tweak

* simplify ui mouse move handling

---------

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>
2023-03-20 09:15:51 +01:00
Alexander Rose
2114c4a3ad type fixes 2023-03-19 15:12:42 -07:00
Alexander Rose
2ca41b2b51 package updates 2023-03-19 15:11:58 -07:00
Alexander Rose
6605a2019e Merge pull request #753 from giagitom/dpoit-avoid-alpha-0-rendering
Dpoit avoid alpha 0 rendering
2023-03-19 14:10:43 -07:00
giagitom
8b1ed5f183 Including wboit 2023-03-19 21:05:25 +01:00
giagitom
f11a1b788f Updated changelog 2023-03-19 19:54:30 +01:00
giagitom
7928e24c54 Avoid rendering of fully transparent renderables 2023-03-19 19:51:01 +01:00
Alexander Rose
5dbca41da6 fix blurry occlusion in screenshots 2023-03-18 19:01:33 -07:00
Alexander Rose
f3fa54addf input/controls improvements 2023-03-18 15:34:46 -07:00
Alexander Rose
e636397f90 ensure marking edges are at least one pixel wide 2023-03-15 20:56:08 -07:00
Sebastian Bittrich
1f3e20704d better applicable check 2023-03-15 16:05:06 -07:00
Sebastian Bittrich
cc9bdd4f14 add ccd hierarchy preset 2023-03-15 16:01:54 -07:00
Russell Parker
6d76bf120d Change nodejs-shim conditional to avoid checking document (#740) 2023-03-08 17:43:38 +01:00
Alexander Rose
a50e81551f use ssao-scale for gl viewport/scissor 2023-03-06 22:48:31 -08:00
Alexander Rose
86512bcea1 tweak ssao-blur thresholds 2023-02-26 19:16:37 -08:00
Alexander Rose
975f45eb01 package updates 2023-02-25 15:02:21 -08:00
Alexander Rose
f2399d3179 Merge pull request #737 from molstar/pp-improvements
Post-processing improvements
2023-02-25 14:45:48 -08:00
Alexander Rose
b26d62a067 webgl1 compat 2023-02-25 14:09:13 -08:00
Alexander Rose
926d6cbd46 reduce over-blurring occlusion at larger view distances 2023-02-25 13:52:18 -08:00
Alexander Rose
7ea47d2a99 use pixel-size for max depth difference 2023-02-25 13:31:44 -08:00
Alexander Rose
89ad8cfc15 fix orthographic camera defines not updated 2023-02-25 13:17:03 -08:00
Alexander Rose
302a309aff add occlussion color 2023-02-25 13:03:12 -08:00
Sebastian Bittrich
fbc74c0012 Merge remote-tracking branch 'upstream/master' 2023-02-24 14:27:24 -08:00
Sebastian Bittrich
27a953795c use ComponentBond.Provider 2023-02-24 14:17:48 -08:00
dsehnal
c3e62bc2e5 3.31.4 2023-02-24 13:13:06 +01:00
dsehnal
c2ab322bd2 Stop animation loop on dispose 2023-02-24 13:10:35 +01:00
jump2cn
aeab0f235c allow link cylinder/line dashCount set to '0' (#735) 2023-02-23 10:52:56 +01:00
dsehnal
ae2285599f 3.31.3 2023-02-22 20:44:32 +01:00
dsehnal
104ab757d2 Update fs import in data-source.ts 2023-02-22 20:37:34 +01:00
Sebastian Bittrich
6ada52bc0b names 2023-02-21 14:33:37 -08:00
Sebastian Bittrich
c526cb9f08 consolidate params 2023-02-21 13:51:26 -08:00
Sebastian Bittrich
a1662d76fb parse CCD files 2023-02-21 13:41:40 -08:00
Alexander Rose
de84a8c8c5 tweak minNear param max 2023-02-18 11:42:45 -08:00
Alexander Rose
4fa135daf0 fix near clipping avoidance in impostor shaders 2023-02-18 11:33:36 -08:00
midlik
9870cb4082 Fixed degenerate case (1-point) in PCA (#725)
* Fixed degenerate case (1-point) in PCA - now correctly returns identity matrix

* Changelog
2023-02-17 20:08:21 +01:00
Alexander Rose
b2924761ab update impostor bond visuals on sizeFactor changes 2023-02-12 22:06:18 -08:00
Alexander Rose
9c5f451878 3.31.2 2023-02-12 09:19:17 -08:00
Alexander Rose
6a5fb85c5a changelog 2023-02-12 09:13:40 -08:00
Alexander Rose
2aef5fb3e5 fix use of WEBGL_provoking_vertex 2023-02-12 09:12:24 -08:00
Jose Manuel Duarte
63a9aef5eb Fixing exit code for volume packer (#714)
* Fixing exit code for pack.ts

* Changelog

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-02-12 14:37:02 +01:00
Alexander Rose
e36fe8c707 fix polymer trace update logic 2023-02-11 21:26:03 -08:00
Alexander Rose
0585f7b9ca timer, mark ssao 2023-02-11 10:53:47 -08:00
Alexander Rose
25ab0d7799 fix outline artefact with opaque behind transparent 2023-02-11 10:49:43 -08:00
Alexander Rose
ea313a442c remove pca transform from components ui focus
- too distracting
2023-02-11 10:45:09 -08:00
Alexander Rose
57a63e0381 3.31.1 2023-02-05 16:35:19 -08:00
Alexander Rose
c525812aee changelog 2023-02-05 16:30:41 -08:00
Alexander Rose
e602705e6d 3.31.0 2023-02-05 16:19:17 -08:00
Alexander Rose
b7d126b39b package updates 2023-02-05 16:13:46 -08:00
Alexander Rose
80b2864da8 remove unused travis file 2023-02-05 16:13:36 -08:00
Alexander Rose
36ba6f035a increase max intensity in light/ambient params 2023-02-05 15:45:39 -08:00
Alexander Rose
b84bbdace2 refactor PCA focus some more 2023-02-05 15:08:17 -08:00
Alexander Rose
750b2f69fc Merge pull request #710 from molstar/console-stats-timing
add consoleStats, improve timer
2023-02-05 14:00:51 -08:00
Alexander Rose
6ffe051a8e Merge branch 'master' of https://github.com/molstar/molstar 2023-02-05 13:43:36 -08:00
Alexander Rose
bc3640b264 add asymIds to OperatorGroup 2023-02-05 13:43:27 -08:00
Alexander Rose
8746b6e2f4 Observable canvas commit 2023-02-05 13:41:42 -08:00
dsehnal
445977d99b Fix data-source node workaround 2023-02-05 22:34:38 +01:00
Alexander Rose
0f0185e18c add fast boundary helper and unit trait 2023-02-05 13:34:33 -08:00
dsehnal
a748b1581e refactor PCA focus 2023-02-05 22:24:40 +01:00
Ke Ma
af1e06203b Dev focus pca (#624)
* viewer camera change based on Pca

* minor code refactor

* update author

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-ui/structure/components.tsx

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* Update src/mol-plugin-state/manager/focus-camera/focus-first-residue.ts

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>

* revise

* deepclone

* chunked-array

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-02-05 20:27:25 +01:00
Alexander Rose
356cf008ce add consoleStats, improve timer 2023-02-04 19:25:08 -08:00
Alexander Rose
07284e7e3d handle null-texture in calcTextureMeshColorSmoothing 2023-02-04 13:58:02 -08:00
Alexander Rose
72055442b7 add Color.contrast/.luminance 2023-02-04 10:30:14 -08:00
Alexander Rose
5fa7d84c23 axes camera-helper tweaks 2023-02-03 07:59:26 -08:00
midlik
de46f08bf4 Minor changes regarding HeadlessPluginContext (#708)
* Minor changes regarding HeadlessPluginContext

* Tweaks
2023-02-03 12:31:39 +01:00
Alexander Rose
fde395e2fa Merge pull request #705 from molstar/camera-helper-params
add axes camera-helper customization options
2023-02-01 08:10:47 -08:00
midlik
da1e55250e Added HeadlessPluginContext, Canvas3DRenderer, image-renderer example (#700)
* Added HeadlessPluginContext, Canvas3DRenderer, image-renderer example

* Fixed dependencies in package.json

* Removed gl from deps

* Removed gl, pngjs, jpeg-js from dependencies, importing via LazyImports

* Updated docs in src/examples/image-renderer/index.ts

* Added gl, pngjs, jpeg-js to optionalDependencies

* Canvas3DRenderer: default cameraResetDurationMs: 0 (necessary when changing camera between renderings)

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2023-02-01 16:56:38 +01:00
Alexander Rose
a4ab117d14 support WEBGL_provoking_vertex 2023-01-31 19:56:03 -08:00
Alexander Rose
4cd7088eb8 add axes camera-helper customization options 2023-01-31 19:41:14 -08:00
Alexander Rose
8e1876fc25 update camera/handle helper on pixelRatio changes 2023-01-31 19:35:09 -08:00
Alexander Rose
29693ebe8c fix text zOffset with orthographic projection 2023-01-31 19:09:36 -08:00
Alexander Rose
a8ee7bfcbe fix uModelView construction 2023-01-31 19:04:46 -08:00
Alexander Rose
2037dad03d 3.30.0 2023-01-29 16:28:11 -08:00
Alexander Rose
0a9fb603f6 changelog 2023-01-29 16:22:25 -08:00
Alexander Rose
0b7dadd345 package updates 2023-01-29 16:21:56 -08:00
Alexander Rose
a3645f5acc use single channel for downsampled depth when possible 2023-01-29 16:08:58 -08:00
Alexander Rose
92f409d6fc fix downsampled ssao 2023-01-29 16:08:18 -08:00
Alexander Rose
c28dd8135c perf tweaks
- reuse vec3 in getMatrices
- fast path in createElementSphereImpostor
2023-01-28 23:08:39 -08:00
Alexander Rose
3baa03ccdc adjust outlines based on view distance 2023-01-22 14:21:37 -08:00
Alexander Rose
659e96d93c Merge pull request #699 from MadCatX/ntc_tube_discontinuity
Fix NtC tube and Confal pyramids mesh when there are discontinuous chains
2023-01-22 14:14:51 -08:00
Alexander Rose
365d7d46fd fix SSAO artefacts with high bias values 2023-01-22 11:47:17 -08:00
Alexander Rose
f8284508e1 Merge pull request #695 from molstar/state-images
state snapshot images
2023-01-22 11:45:16 -08:00
Alexander Rose
e9c36c2375 fix boundary-helper regression 2023-01-19 21:43:57 -08:00
Michal Malý
5d269fd77c Check for discontinuities when buiilding Confal pyramids mesh 2023-01-17 17:30:30 +01:00
Michal Malý
0064293e01 Unregister ConfalPyramids 2023-01-17 17:30:30 +01:00
Michal Malý
ba4b6f70d3 Draw gaps in NtC tube if there are discontinuities in a chain 2023-01-17 17:30:28 +01:00
Alexander Rose
9d056a85ec state snapshot images 2023-01-15 22:59:33 -08:00
Alexander Rose
3ed17fce6b update gh workflow gl version 2023-01-15 22:53:34 -08:00
Alexander Rose
d656dd0d18 improve boundary calculation performance 2023-01-15 22:49:07 -08:00
Alexander Rose
27c92085c7 changelog 2023-01-15 14:06:53 -08:00
Alexander Rose
94e9462af8 Merge pull request #526 from MadCatX/ntc_tube_2
Add NtC Tube representation
2023-01-15 14:05:29 -08:00
Michal Malý
2e52ccf5d8 Add NtC Tube representation 2023-01-15 22:18:13 +01:00
Michal Malý
b15196a284 Reset ConfalPyramidsIterator internal state correctly when switching
chains
2023-01-15 22:08:45 +01:00
Michal Malý
d29cc85439 Factor out common Dnatco code 2023-01-15 22:08:41 +01:00
Alexander Rose
30367cf239 lint spacing 2023-01-15 11:10:16 -08:00
Alexander Rose
dcf9d1c3bb 3.29.0 2023-01-15 10:06:42 -08:00
Alexander Rose
c0c2e4ce4a changelog 2023-01-15 10:01:35 -08:00
Alexander Rose
3557f4e738 schema updates 2023-01-15 10:00:38 -08:00
Alexander Rose
6595c00cff package updates 2023-01-15 09:58:20 -08:00
Alexander Rose
c612018ba8 Merge pull request #693 from molstar/dim-unmarked
support dim unmarked groups & marker edge strength
2023-01-15 09:04:35 -08:00
Alexander Rose
ec06ef2dfb fix missing uniforms 2023-01-14 13:14:16 -08:00
Alexander Rose
2febf45700 support dim unmarked groups & marker edge strength 2023-01-14 12:46:26 -08:00
Alexander Rose
3e6066a1a1 add .getCenter and .center to Camera 2023-01-14 12:41:41 -08:00
Alexander Rose
b61e5c76db add default to ParamDefinition.Values type 2023-01-14 12:40:13 -08:00
Alexander Rose
bdee4859f2 generic loci support for overpaint, substance, clipping 2023-01-14 12:39:25 -08:00
Alexander Rose
b5e45aae95 Merge pull request #692 from molstar/misc2
Misc2
2023-01-09 20:18:01 -08:00
Alexander Rose
7fd12c5622 tweaks/fixes 2023-01-09 20:16:33 -08:00
Alexander Rose
47442be989 changelog 2023-01-08 21:30:41 -08:00
Alexander Rose
3a484dc41a make SymmetryOperator.createMapping monomorphic 2023-01-08 21:25:23 -08:00
Alexander Rose
9a2dfd7e57 improve calculateInvariantBoundingSphere 2023-01-08 21:25:07 -08:00
Alexander Rose
c9a168a138 improve checks in UnitsRepresentation setVisualState 2023-01-08 21:24:35 -08:00
Alexander Rose
3726f28eeb defer calculation of more scene properties 2023-01-08 21:24:21 -08:00
Alexander Rose
ead25d6a9c tweak BoundaryHelper performance 2023-01-08 21:24:14 -08:00
Alexander Rose
55f4abb6be add repr/theme registry .clear method 2023-01-08 21:24:04 -08:00
dsehnal
2e013fafc8 Add StructureElement.Loci.forEachLocation 2023-01-05 13:07:21 +01:00
dsehnal
0809379f91 update build badge 2023-01-02 14:23:36 +01:00
dsehnal
0d4a95f5af workflow name 2023-01-02 14:21:30 +01:00
David Sehnal
2e9129a71c Fix some cyclic imports & reduce const enum usage (#680)
* fix mol-theme/color

* fix mol-theme/size

* remove unnecessary const enums

* remove Column.ValueType const enum

* NumberTypes

* ValueKindConst => ValueKinds

* more const enum fixes

* more const enums

* readme

* update filename

* typing

* FeatureTypes
2022-12-31 11:22:18 +01:00
David Sehnal
7384bebf4e Fix some cyclic imports & reduce const enum usage (#680)
* fix mol-theme/color

* fix mol-theme/size

* remove unnecessary const enums

* remove Column.ValueType const enum

* NumberTypes

* ValueKindConst => ValueKinds

* more const enum fixes

* more const enums

* readme

* update filename

* typing

* FeatureTypes
2022-12-31 11:22:05 +01:00
Alexander Rose
0e197b1885 Merge pull request #683 from molstar/misc
Misc tweaks
2022-12-30 20:45:46 -08:00
Alexander Rose
54697ae0d5 changelog 2022-12-30 10:16:30 -08:00
Alexander Rose
7b6eb9337e add input-color picker 2022-12-29 21:51:42 -08:00
Alexander Rose
a94fb84052 add isDisabled to ParameterMappingControl 2022-12-29 21:51:27 -08:00
Alexander Rose
528aeb1873 only update clip defines if changed 2022-12-29 21:51:18 -08:00
Alexander Rose
e7b35daf45 improve structure/unit areEqual methods
- always equal when objects are identical
2022-12-29 21:51:06 -08:00
Alexander Rose
7d10971617 avoid cloning of structure repr params 2022-12-29 21:50:49 -08:00
dsehnal
3d83211503 fix dropFiles bug 2022-12-24 12:51:09 +01:00
dsehnal
cd8c8fa020 Merge branch 'master' of https://github.com/molstar/molstar 2022-12-23 14:28:16 +01:00
David Sehnal
4c03009357 Volume Quick Color Select Control (#677)
* changelog

* volume quick select control

* tweak
2022-12-22 08:53:27 +01:00
Alexander Rose
047f547863 support tags in StructureBuilder.createStructure 2022-12-21 20:25:03 -08:00
Alexander Rose
83c62c614b Merge branch 'master' of https://github.com/molstar/molstar 2022-12-21 20:24:22 -08:00
Alexander Rose
c39b3569de add escapeRegExp util 2022-12-21 20:24:06 -08:00
dsehnal
7fea62aa46 changelog 2022-12-20 14:28:07 +01:00
midlik
d816f510ea Volseg more tweaks 2 (#675)
* Volseg: Fixed not loading volume section from state

* Volseg: Double-sided meshes (still ugly)

* Mesh extension: Detection (and correction) of inverted meshes

* Meshes extension: Fixed transparent backfaces, tidy up

* Removed duplicated classes in Meshes and Volseg extensions

* Changelog
2022-12-20 14:24:31 +01:00
dsehnal
c2064aa318 3.28.0 2022-12-20 00:37:33 +01:00
dsehnal
109709ce8b Merge branch 'master' of https://github.com/molstar/molstar 2022-12-20 00:34:53 +01:00
midlik
b200725762 Volseg more tweaks (#674)
* Volseg: Fixed not loading volume section from state

* Volseg: Double-sided meshes (still ugly)

* Mesh extension: Detection (and correction) of inverted meshes
2022-12-20 00:34:40 +01:00
dsehnal
9cf34bf987 changelog 2022-12-20 00:27:00 +01:00
Alexander Rose
be3a91bb14 package updates 2022-12-19 15:21:10 -08:00
Alexander Rose
fc948d8b27 fix gpu mc use of float16 2022-12-19 14:41:12 -08:00
midlik
8774658f4e Volseg: Fixed not loading volume section from state (#672)
* Volseg: Fixed not loading volume section from state

* Volseg: Double-sided meshes (still ugly)
2022-12-19 23:19:53 +01:00
David Sehnal
0bf32148af Viewer setting for volumes and segmentations (#673) 2022-12-19 23:10:40 +01:00
David Sehnal
97d158b615 Volseg extenison tweaks (#671)
* improve currentVolume behavior

* use different iso-value if 1sigma isnt present in the data
2022-12-19 22:16:09 +01:00
Alexander Rose
b772dea188 tweak ignoreHydrogens non-polar handling (#669) 2022-12-19 21:59:55 +01:00
Alexander Rose
5c8f0b35ec changelog 2022-12-19 10:33:36 -08:00
dsehnal
1b382653f2 add volume controls to Volseg extension 2022-12-19 16:49:30 +01:00
midlik
c7cee63c97 Volumes & Segmentations extension (#668)
* Created CellStar state action

* CellStar: download metadata

* Right panel - CellStar UI

* CellStar: Lattice segmenatation and fitted PDB models

* CellStar: Support for source-database entry ID from metadata

* CellStar: Mesh segmentation

* CellStar: Switching between multiple entries

* CellStar: Changed default API URL

* UI updates

* CellStar: Clicking on meshes shows annotations...

* support info in Converted param

* support color/size in VolumeRepresentation3DHelpers.getDefaultParams

* support multi-visual volume representations

- repr can provide list of keys
- create visual for each key

* add volume segmentation support

- segmentation property
- segment loci & location

* add volume-segment color theme

* add volume segment representation

* use optional chaining

* add support for volume segmentation cif

* improve isosurface bounding-sphere

* fix segcif schema type

* CellStar: Highlighting segments on hover in the right panel

* CellStar: Using new Mol*-native segment visualization

* lint

* Segmentation volume can have custom segment labels

* CellStar: Segment labels for Mol*-native segments

* CellStar: Removed old implementation of segments

* CellStar: Rename CellStarLatticeSegmentationData2 -> CellStarLatticeSegmentationData

* CellStar: Default volume server is https://cellstar.ncbr.muni.cz

* CellStar: debugging

* CellStar: Fixed bug in LatticeSegmentation (scaling)

* CellStar: Partially savable state

* CellStar: WaitingSlider

* CellStar: Opacity changed via params

* Savable state for opacity

* CellStar: Changing UI in animations

* CellStar: Savable state for whole current UI

* CellStar: Savable state for segment labels

* CellStar: Source can be 'idr', CellStarVolumeServerConfig.DefaultServer

* CellStar: Select segment for lattice segmentation

* CellStar: Select segment, complete

* CellStar: Changes visible labels to "Volume & Segmentation"

* CellStar: Drop list with available entries

* CellStar: Volume type switching (partial)

* CellStar: Trying to set direct-volume control points

* CellStar: Volume visual switching

* Mesh extension: removed molstar-lib-imports

* CellStar: Global options

* CellStar: Updated file headers and `CHANGELOG.md`

* CellStar: UI controls disabled while executing change

* CellStar: Hidden state nodes, fixed bug with removed global state

* CellStar: Volume opacity slider

* CellStar extension renamed to Volseg

* UI tweaks

Co-authored-by: Alexander Rose <alexander.rose@weirdbyte.de>
2022-12-19 16:15:23 +01:00
Alexander Rose
ba9474fa62 add stencil related webgl state and format 2022-12-16 21:47:05 -08:00
Alexander Rose
b7ec7ea686 Merge pull request #665 from molstar/solid-impostor
Solid impostors
2022-12-16 21:41:14 -08:00
Alexander Rose
74560adb08 changelog 2022-12-16 21:20:52 -08:00
Alexander Rose
e6a303bdda Merge branch 'master' of https://github.com/molstar/molstar into solid-impostor 2022-12-16 21:19:19 -08:00
David Sehnal
7afcf0bb68 Show histogram in direct volume control point settings (#666) 2022-12-16 20:02:46 +01:00
Alexander Rose
7aae2d0616 fix impostor sphere near clipping 2022-12-15 22:06:06 -08:00
dsehnal
9255505a3b 3.27.0 2022-12-15 13:02:37 +01:00
dsehnal
6c0dfd338d changelog 2022-12-15 12:59:45 +01:00
David Sehnal
c306e988c8 Ability to show only polar hydrogens (#663) 2022-12-15 12:26:34 +01:00
Alexander Rose
86f7a8b273 add impostor solidInterior param 2022-12-14 22:50:15 -08:00
David Sehnal
2428a8efab Merge pull request #660 from molstar/struct-conn-ions-fix
Fix struct_conn bond assignment for ions
2022-12-14 10:20:22 +01:00
Alexander Rose
cba12e487f wip, solid impostor flag 2022-12-13 23:59:08 -08:00
dsehnal
e98d7931ca Fix import 2022-12-13 18:01:59 +01:00
dsehnal
f8e2d2e3d0 tweak hasStructConnRecord 2022-12-13 17:57:31 +01:00
dsehnal
4455bab6ac Fix struct_conn bond assignment for ions 2022-12-13 16:06:19 +01:00
David Sehnal
b21fb27041 Merge pull request #658 from molstar/action-params
ApplyActionControl param handling
2022-12-13 15:20:31 +01:00
dsehnal
5402ee0019 ApplyActionControl param handling 2022-12-13 15:16:19 +01:00
David Sehnal
02654ea57b Merge pull request #657 from molstar/fix-qa-assignment
Fix QualityAssessment assignment
2022-12-13 09:33:13 +01:00
dsehnal
9cd4aeabaa Merge branch 'master' of https://github.com/molstar/molstar into fix-qa-assignment 2022-12-13 09:32:42 +01:00
dsehnal
d788511a83 header 2022-12-13 09:32:20 +01:00
Alexander Rose
378b5b8096 Merge pull request #643 from arussell123/master
Add include transparent parameter for outlines
2022-12-12 21:58:49 -08:00
Alexander Rose
e26eb2f751 Merge branch 'master' into master 2022-12-12 21:58:25 -08:00
Alexander Rose
26264b15f7 tweaks 2022-12-12 21:56:34 -08:00
Alexander Rose
309e3dd981 impostor per-pixel object clipping 2022-12-12 21:51:39 -08:00
Alexander Rose
4ff5ed3b5d fix cylinder imposter cap normals 2022-12-12 21:18:37 -08:00
dsehnal
270a757756 changelog 2022-12-12 18:39:41 +01:00
dsehnal
79e4030ab4 fix qa assignment 2022-12-12 18:35:52 +01:00
David Sehnal
fd86927e04 Merge pull request #655 from MKampfrath/access-modifiers
change access modifiers
2022-12-12 16:36:38 +01:00
MKampfrath
df3a7e5037 change access modifiers 2022-12-12 16:30:40 +01:00
Alice Russell
fefc6f14c9 Change is outline enabled to is transparant outline enabled 2022-12-12 10:43:14 +00:00
Alexander Rose
923fbef0e4 remove superfluous check 2022-12-11 20:45:26 -08:00
Alexander Rose
601bd5ba7a improve impostor shaders
- fix sphere near-clipping with orthographic projection
- fix cylinder near-clipping
- add interior cylinder caps
2022-12-10 21:47:16 -08:00
Alexander Rose
5798f20c41 lint fix 2022-12-10 21:41:25 -08:00
David Sehnal
f2e26f91a8 Merge pull request #647 from dwilliams-nobias/animation-once-stops
Animation "once" fails to stop for many frames under certain conditions
2022-12-08 16:53:27 +01:00
David Sehnal
e2cdc45be6 Merge branch 'master' into animation-once-stops 2022-12-08 16:53:15 +01:00
Alice Russell
96493bc75e Fix linting 2022-12-07 14:26:48 +00:00
Alice Russell
d78ad4a78e Merge remote-tracking branch 'upstream/master' 2022-12-07 12:25:22 +00:00
Alice Russell
0c54b0dd6e Add option to toogle transparent outline 2022-12-07 12:21:31 +00:00
Alexander Rose
65310e52de guard against issue with bumpiness in impostors 2022-12-06 22:48:22 -08:00
David Williams
285d3cd9b8 Requested changes for pull request 2022-12-06 17:36:48 -08:00
David Williams
10486abb64 Add example molecule with 1,000 animation frames 2022-12-06 17:32:44 -08:00
David Williams
8c1a9fc988 Correctly check for duration "once" for fixed or computed duration 2022-12-06 16:57:27 -08:00
Alexander Rose
7bdd0b470b 3.26.0 2022-12-04 12:18:09 -08:00
Alexander Rose
b7a51f12bf changelog 2022-12-04 12:12:22 -08:00
Alexander Rose
e002ac5474 Merge pull request #644 from molstar/power-pref
support power-preference webgl attribute
2022-12-04 12:09:46 -08:00
Alexander Rose
211d339ce0 Merge branch 'master' into power-pref 2022-12-04 12:09:39 -08:00
Alexander Rose
509fb14473 Merge pull request #645 from molstar/structure-mol-surf
add structure molecular surface visual
2022-12-04 12:09:14 -08:00
Alexander Rose
ef838a1b83 Merge branch 'master' into structure-mol-surf 2022-12-04 12:09:05 -08:00
Alexander Rose
f84b5b633d improve Representation typing 2022-12-04 09:45:12 -08:00
Alexander Rose
8ec8c1170d comment 2022-12-04 09:32:02 -08:00
David Sehnal
920b98e4d1 Merge pull request #604 from molstar/volume-theme
by-volume-value theme
2022-12-04 09:18:32 +01:00
dsehnal
c80f52d4bc Merge branch 'master' of https://github.com/molstar/molstar into volume-theme 2022-12-04 09:14:41 +01:00
dsehnal
0b6aa42daf tweaks 2022-12-04 09:14:06 +01:00
Alexander Rose
04dc6427ef update SaccharideNames 2022-12-03 22:00:24 -08:00
Alexander Rose
77e366b484 schema updates 2022-12-03 21:58:04 -08:00
Alexander Rose
add7cfa0f2 package updates 2022-12-03 21:53:00 -08:00
Alexander Rose
14d4fa142c changelog 2022-12-03 21:46:55 -08:00
Alexander Rose
7fe1617f25 shadow param tweaks 2022-12-03 21:46:30 -08:00
Alexander Rose
cbe4cb521c add structure molecular surface visual 2022-12-03 21:32:54 -08:00
Alexander Rose
7c394525c1 Merge pull request #637 from molstar/sss
Screen-space shadows
2022-12-03 10:59:05 -08:00
Alexander Rose
12ed051fea Merge pull request #606 from jpattle/remove-caps-from-ligand-query
Remove common protein caps from ligand query
2022-12-03 10:41:00 -08:00
Alexander Rose
014913c635 support power-preference webgl attribute 2022-12-03 10:17:57 -08:00
Alexander Rose
369e45c282 handle bounds in shadow screen fade 2022-12-03 10:02:14 -08:00
Alice Russell
2892caec0c Update changelog 2022-12-02 12:31:56 +00:00
Alice Russell
eb609e918a linting fixes 2022-12-02 12:28:51 +00:00
Alice Russell
9217e58845 Add option to set minimum alpha value for outlines to be shown 2022-12-02 12:12:22 +00:00
Alexander Rose
26b633618a tweak cellpack shadow params 2022-11-27 22:22:29 -08:00
Alexander Rose
e3054d6ee1 shadows bonds check 2022-11-27 22:07:23 -08:00
Alexander Rose
0b6294f4ec check for shadow in PostprocessingPass.isEnabled 2022-11-26 22:33:11 -08:00
Alexander Rose
7b130dc0f3 cleanup 2022-11-26 22:09:24 -08:00
Alexander Rose
d93c128c25 cleanup 2022-11-26 22:04:41 -08:00
Alexander Rose
e1dd74775e refactor sss
- only sss, remove other ao implementations
- tweak params
- make sss a separate renderable
2022-11-26 21:47:08 -08:00
Alexander Rose
e61e706607 Merge branch 'master' of https://github.com/molstar/molstar into fake_shadow 2022-11-25 21:13:35 -08:00
jpattle
a57d46deaa Merge branch 'master' into remove-caps-from-ligand-query 2022-11-21 08:58:41 +00:00
Alexander Rose
60efdc0071 3.25.1 2022-11-20 10:43:31 -08:00
Alexander Rose
4dc1155a9e changelog 2022-11-20 10:38:07 -08:00
Alexander Rose
5cabe6fb42 fix auto structure-quality for coarse models 2022-11-20 10:37:30 -08:00
Alexander Rose
42239c305f better handle single-element units in Structure.eachUnitPair 2022-11-20 10:25:26 -08:00
ludovic autin
fd3c763349 All parameters exposed to try to find best parameters for different representation. 2022-11-18 09:13:42 -08:00
ludovic autin
daa9bbf2c9 fix trailing space 2022-11-17 09:31:21 -08:00
ludovic autin
c7dad00908 fake screen space shadow, second pass ssao 2022-11-17 09:28:00 -08:00
ludovic autin
24317717e8 Merge remote-tracking branch 'upstream/master' into forkdev 2022-11-16 14:01:04 -08:00
dsehnal
dd3eac6db6 3.25.0 2022-11-16 00:15:41 +01:00
dsehnal
1606f8517f changelog 2022-11-16 00:12:57 +01:00
David Sehnal
3a98401ada Merge pull request #623 from JonStargaryen/master
Fix Handling of Gzipped Assets by Reverting #615
2022-11-16 00:11:02 +01:00
Alexander Rose
4764251241 operator key tweaks 2022-11-14 21:48:44 -08:00
Sebastian Bittrich
3de04b7b9d CHANGELOG 2022-11-14 13:17:15 -08:00
Sebastian Bittrich
04908495e9 Revert "keep asset url to detect compression"
This reverts commit 2bc45c25fe.
2022-11-14 13:13:06 -08:00
Alexander Rose
3c835c848e 3.24.0 2022-11-13 20:56:58 -08:00
Alexander Rose
0f1788f122 changelog 2022-11-13 20:51:24 -08:00
Alexander Rose
3d72e700a4 package updates 2022-11-13 20:48:33 -08:00
Alexander Rose
a5cf41e65f fix viewport color for transparent background (#617) 2022-11-13 20:47:30 -08:00
Alexander Rose
7029bc41d7 Merge pull request #621 from molstar/theme-strength
add theme strength to 3d repr state
2022-11-13 20:22:14 -08:00
Alexander Rose
b87d40c844 changelog 2022-11-13 20:21:41 -08:00
Alexander Rose
9e154376d3 Merge branch 'master' of https://github.com/molstar/molstar into theme-strength 2022-11-13 20:18:56 -08:00
Alexander Rose
4a9fdbce57 Merge pull request #619 from molstar/opkey
add key to symmetry operator
2022-11-13 20:18:12 -08:00
Alexander Rose
775e335292 remove cantor-pair use 2022-11-13 20:11:42 -08:00
Alexander Rose
e553bf4deb add theme strength to 3d repr state 2022-11-12 21:58:13 -08:00
Alexander Rose
db0e09ec6e add more webgl extensions
- draw_buffers_indexed
- parallel_shader_compile
- fbo_render_mipmap
2022-11-12 20:34:18 -08:00
Alexander Rose
ba50760f92 add key to symmetry operator
- molql operatorKey
- index-pair operator prop
2022-11-12 20:30:33 -08:00
David Sehnal
524e6d4f81 Merge pull request #615 from JonStargaryen/master
keep asset url to detect compression
2022-11-11 07:47:44 +01:00
ludovic autin
db93a669ab Merge branch 'molstar:master' into forkdev 2022-11-10 13:59:26 -08:00
Sebastian Bittrich
423f5b0502 CHANGELOG 2022-11-10 13:16:27 -08:00
Sebastian Bittrich
2bc45c25fe keep asset url to detect compression 2022-11-10 13:13:49 -08:00
Jason Pattle
d341463f67 Removed unneccessary wholeResidues query from new ligand selection query as the atomGroups residue-test already covers this functionality 2022-11-07 08:37:16 +00:00
Alexander Rose
19d1ecb36a add (slow) texture-mesh position iterator 2022-11-06 23:27:17 -08:00
Alexander Rose
ffe4047f97 fix vertex color/overpaint in direct-volume shader 2022-11-06 22:24:36 -08:00
Alexander Rose
15a3c29e7a adjust external-volume theme file name 2022-11-06 22:23:15 -08:00
Jason Pattle
42ff593004 Added missing semicolon; linting 2022-11-02 10:06:26 +00:00
Jason Pattle
5140af4e6f Added myself as a contributor to package.json 2022-11-02 09:56:07 +00:00
Jason Pattle
0f5b4c00a9 Updated changelog 2022-11-02 09:54:57 +00:00
Jason Pattle
feb69f4987 Excluded whole residues which match any common protein caps from the ligand selection query 2022-11-02 09:49:44 +00:00
Jason Pattle
84eb9a58ca Added names for common protein caps 2022-11-02 09:49:07 +00:00
dsehnal
fbd96f473a External Volume theme features 2022-11-01 11:54:27 +01:00
dsehnal
69228157dc fix typo 2022-10-31 15:42:13 +01:00
dsehnal
6808f32b8d by-volume-value theme 2022-10-31 15:41:10 +01:00
dsehnal
509e6bc2d8 add TrajectoryFromMmCif.loadAllBlocks param 2022-10-09 14:36:33 +02:00
ludovic autin
9e69f5dcfa Merge branch 'master' of https://github.com/corredD/molstar-proto into forkdev
# Conflicts:
#	CHANGELOG.md
2022-07-05 08:51:25 -07:00
ludovic autin
409ed9ad67 Merge branch 'forkdev' of https://github.com/corredD/molstar-proto into forkdev
# Conflicts:
#	CHANGELOG.md
2022-06-28 08:11:29 -07:00
ludovic autin
06e78e19d0 Merge branch 'forkdev' of https://github.com/corredD/molstar-proto into forkdev 2022-05-31 09:55:03 -07:00
ludovic autin
9a1d746170 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-11-17 10:06:49 -08:00
ludovic autin
ef87ce3507 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-11-10 14:35:24 -08:00
ludovic autin
2b9053eac4 Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-11-01 10:21:08 -07:00
ludovic autin
116ef719be Merge branch 'master' of https://github.com/molstar/molstar into forkdev 2021-09-20 09:17:09 -07:00
421 changed files with 127532 additions and 8946 deletions

View File

@@ -18,7 +18,7 @@
],
"brace-style": "off",
"comma-spacing": "off",
"space-infix-ops": "error",
"space-infix-ops": "off",
"comma-dangle": "off",
"eqeqeq": [
"error",
@@ -107,6 +107,7 @@
"1tbs", { "allowSingleLine": true }
],
"@typescript-eslint/comma-spacing": "error",
"@typescript-eslint/space-infix-ops": "error",
"@typescript-eslint/space-before-function-paren": ["error", {
"anonymous": "always",
"named": "never",

View File

@@ -1,3 +1,5 @@
name: Build
on:
push:
pull_request:
@@ -15,6 +17,6 @@ jobs:
- name: Lint
run: npm run lint
- name: Test
run: npm install --no-save "gl@^5.0.0" && xvfb-run --auto-servernum npm run jest
run: npm install --no-save "gl@^6.0.2" && xvfb-run --auto-servernum npm run jest
- name: Build
run: npm run build

View File

@@ -1,18 +0,0 @@
language: node_js
os: linux
sudo: required
dist: trusty
before_install:
- sudo apt-get install -y mesa-utils
- sudo apt-get install -y xvfb
- sudo apt-get install -y libgl1-mesa-dri
- sudo apt-get install -y libglapi-mesa
- sudo apt-get install -y libosmesa6
- sudo apt-get install -y gcc-4.9
- sudo apt-get install -y libstdc++6
- sudo apt-get install -y libxi-dev
node_js:
- "12"
- "10"
before_script:
- export DISPLAY=:99.0; sh -e /etc/init.d/xvfb start

View File

@@ -6,18 +6,228 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [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
- Remove pca transform from components ui focus (too distracting)
- Fix artefacts with opaque outlines behind transparent objects
- Fix polymer trace visual not updating
- Fix use of `WEBGL_provoking_vertex`
## [v3.31.1] - 2023-02-05
- Improve Component camera focus based on the PCA of the structure and the following rules:
- The first residue should be in first quadrant if there is only one chain
- The average position of the residues of the first chain should be in the first quadrant if there is more than one chain
- Add `HeadlessPluginContext` and `HeadlessScreenshotHelper` to be used in Node.js
- Add example `image-renderer`
- Fix wrong offset when rendering text with orthographic projection
- Update camera/handle helper when `devicePixelRatio` changes
- Add various options to customize the axes camera-helper
- Fix issue with texture-mesh color smoothing when changing themes
- Add fast boundary helper and corresponding unit trait
- Add Observable for Canvas3D commits
## [v3.30.0] - 2023-01-29
- Improve `Dnatco` extension
- Factor out common code in `Dnatco` extension
- Add `NtC tube` visual. Applicable for structures with NtC annotation
- [Breaking] Rename `DnatcoConfalPyramids` to `DnatcoNtCs`
- Improve boundary calculation performance
- Add option to create & include images in state snapshots
- Fix SSAO artefacts with high bias values
- Fix SSAO resolution scale parameter handling
- Improve outlines, visually more stable at different view distances
## [v3.29.0] - 2023-01-15
- `meshes` extension: Fixed a bug in mesh visualization (show backfaces when opacity < 1)
- Add color quick select control to Volume controls
- Fix `dropFiles` bug
- Fix some cyclic imports and reduce the use of const enums. This should make it easier to use the library with the `isolatedModules: true` TS config.
- Fix `dropFiles` bug (#679)
- Add `input type='color'` picker to `CombinedColorControl`
- Set `ParameterMappingControl` disabled when state is updating
- Performance tweaks
- Update clip `defines` only when changed
- Check for identity in structure/unit areEqual methods
- Avoid cloning of structure representation parameters
- Make SymmetryOperator.createMapping monomorphic
- Improve bonding-sphere calculation
- Defer Scene properties calculation (markerAverage, opacityAverage, hasOpaque)
- Improve checks in in UnitsRepresentation setVisualState
- Add StructureElement.Loci.forEachLocation
- Add RepresentationRegistry.clear and ThemeRegistry.clear
- Add generic Loci support for overpaint, substance, clipping themes
- Add `.getCenter` and `.center` to `Camera`
- Add support to dim unmarked groups
- Add support for marker edge strength
## [v3.28.0] - 2022-12-20
- Show histogram in direct volume control point settings
- Add `solidInterior` parameter to sphere/cylinder impostors
- [Breaking] Tweak `ignoreHydrogens` non-polar handling (introduced in 3.27.0)
- Add `meshes` and `volumes-and-segmentations` extensions
- See https://molstarvolseg.ncbr.muni.cz/ for more info
- Fix missing support for info in `ParamDefinition.Converted`
- Add support for multi-visual volume representations
- Improve volume isosurface bounding-sphere
- Add basic volume segmentation support to core
- Add `Volume.Segment` model
- Add `Segmentation` custom volume property
- Add `SegmentRepresentation` representation
- Add `volume-segment` color theme
- Fix GPU marching cubes failing for large meshes with webgl2 (due to use of float16)
## [v3.27.0] - 2022-12-15
- Add an `includeTransparent` parameter to hide/show outlines of components that are transparent
- Fix 'once' for animations of systems with many frames
- Better guard against issue (black fringes) with bumpiness in impostors
- Improve impostor shaders
- Fix sphere near-clipping with orthographic projection
- Fix cylinder near-clipping
- Add interior cylinder caps
- Add per-pixel object clipping
- Fix `QualityAssessment` assignment bug for structures with different auth vs label sequence numbering
- Refresh `ApplyActionControl`'s param definition when toggling expanded state
- Fix `struct_conn` bond assignment for ions
- Ability to show only polar hydrogens
## [v3.26.0] - 2022-12-04
- Support for ``powerPreference`` webgl attribute. Add ``PluginConfig.General.PowerPreference`` and ``power-preference`` Viewer GET param.
- Excluded common protein caps `NME` and `ACE` from the ligand selection query
- Add screen-space shadow post-processing effect
- Add "Structure Molecular Surface" visual
- Add `external-volume` theme (coloring of arbitrary geometries by user-selected volume)
## [v3.25.1] - 2022-11-20
- Fix edge-case in `Structure.eachUnitPair` with single-element units
- Fix 'auto' structure-quality for coarse models
## [v3.25.0] - 2022-11-16
- Fix handling of gzipped assets (reverts #615)
## [v3.24.0] - 2022-11-13
- Make `PluginContext.initContainer` checkered canvas background optional
- Store URL of downloaded assets to detect zip/gzip based on extension (#615)
- Add optional `operator.key`; can be referenced in `IndexPairBonds`
- Add overpaint/transparency/substance theme strength to representations
- Fix viewport color for transparent background
## [v3.23.0] - 2022-10-19
- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI
- Add `PluginContext.canvas3dInitialized`
- `createPluginUI` now resolves after the 3d canvas has been initialized
- Change EM Volume Streaming default from `Whote Structure` to `Auto`
- Change EM Volume Streaming default from `Whole Structure` to `Auto`
## [v3.22.0] - 2022-10-17
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality`
## [v3.21.0] - 2022-10-17

View File

@@ -1,6 +1,6 @@
[![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE)
[![npm version](https://badge.fury.io/js/molstar.svg)](https://www.npmjs.com/package/molstar)
[![Build Status](https://travis-ci.org/molstar/molstar.svg?branch=master)](https://travis-ci.org/molstar/molstar)
[![Build](https://github.com/molstar/molstar/actions/workflows/node.yml/badge.svg)](https://github.com/molstar/molstar/actions/workflows/node.yml)
[![Gitter](https://badges.gitter.im/molstar/Lobby.svg)](https://gitter.im/molstar/Lobby)
# Mol*

View 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>
```

View File

@@ -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

File diff suppressed because it is too large Load Diff

28000
examples/long_animation.sdf Normal file

File diff suppressed because it is too large Load Diff

10812
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "3.23.0",
"version": "3.36.1",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -13,7 +13,7 @@
"scripts": {
"lint": "eslint .",
"lint-fix": "eslint . --fix",
"test": "npm install --no-save \"gl@^5.0.0\" && npm run lint && jest",
"test": "npm install --no-save \"gl@^6.0.2\" && npm run lint && jest",
"jest": "jest",
"build": "npm run build-tsc && npm run build-extra && npm run build-webpack",
"clean": "node ./scripts/clean.js",
@@ -92,76 +92,90 @@
"Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
"Adam Midlik <midlik@gmail.com>",
"Koya Sakuma <koya.sakuma.work@gmail.com>",
"Gianluca Tomasello <giagitom@gmail.com>"
"Gianluca Tomasello <giagitom@gmail.com>",
"Ke Ma <mark.ma@rcsb.org>",
"Jason Pattle <jpattle@exscientia.co.uk>",
"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>"
],
"license": "MIT",
"devDependencies": {
"@graphql-codegen/add": "^3.2.1",
"@graphql-codegen/cli": "^2.13.7",
"@graphql-codegen/time": "^3.2.1",
"@graphql-codegen/typescript": "^2.7.4",
"@graphql-codegen/add": "^5.0.0",
"@graphql-codegen/cli": "^4.0.1",
"@graphql-codegen/time": "^5.0.0",
"@graphql-codegen/typescript": "^4.0.0",
"@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
"@graphql-codegen/typescript-graphql-request": "^4.5.6",
"@graphql-codegen/typescript-operations": "^2.5.4",
"@types/cors": "^2.8.12",
"@types/gl": "^4.1.1",
"@types/jest": "^29.1.2",
"@types/react": "^18.0.21",
"@types/react-dom": "^18.0.6",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"@graphql-codegen/typescript-graphql-request": "^5.0.0",
"@graphql-codegen/typescript-operations": "^4.0.0",
"@types/cors": "^2.8.13",
"@types/gl": "^6.0.2",
"@types/jpeg-js": "^0.3.7",
"@types/pngjs": "^6.0.1",
"@types/jest": "^29.5.2",
"@types/react": "^18.2.11",
"@types/react-dom": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^5.59.9",
"@typescript-eslint/parser": "^5.59.9",
"benchmark": "^2.1.4",
"concurrently": "^7.4.0",
"cpx2": "^4.2.0",
"concurrently": "^8.2.0",
"cpx2": "^4.2.3",
"crypto-browserify": "^3.12.0",
"css-loader": "^6.7.1",
"eslint": "^8.25.0",
"css-loader": "^6.8.1",
"eslint": "^8.42.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
"fs-extra": "^10.1.0",
"fs-extra": "^11.1.1",
"graphql": "^16.6.0",
"http-server": "^14.1.1",
"jest": "^29.2.0",
"mini-css-extract-plugin": "^2.6.1",
"jest": "^29.5.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.55.0",
"sass-loader": "^13.1.0",
"simple-git": "^3.14.1",
"sass": "^1.63.3",
"sass-loader": "^13.3.2",
"simple-git": "^3.19.0",
"stream-browserify": "^3.0.0",
"style-loader": "^3.3.1",
"ts-jest": "^29.0.3",
"typescript": "^4.8.4",
"webpack": "^5.74.0",
"webpack-cli": "^4.10.0"
"style-loader": "^3.3.3",
"ts-jest": "^29.1.0",
"typescript": "^5.1.3",
"webpack": "^5.86.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.14",
"@types/node": "^16.11.66",
"@types/node-fetch": "^2.6.2",
"@types/express": "^4.17.17",
"@types/node": "^16.18.35",
"@types/node-fetch": "^2.6.4",
"@types/swagger-ui-dist": "3.30.1",
"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.15",
"immutable": "^4.1.0",
"node-fetch": "^2.6.7",
"rxjs": "^7.5.7",
"swagger-ui-dist": "^4.14.3",
"tslib": "^2.4.0",
"util.promisify": "^1.1.1",
"immer": "^9.0.21",
"immutable": "^4.3.0",
"node-fetch": "^2.6.11",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^4.19.0",
"tslib": "^2.5.3",
"util.promisify": "^1.1.2",
"xhr2": "^0.2.1"
},
"peerDependencies": {
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
},
"optionalDependencies": {
"gl": "^6.0.2",
"jpeg-js": "^0.4.4",
"pngjs": "^6.0.0"
}
}

View File

@@ -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) {

View File

@@ -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>
*/
@@ -31,7 +31,8 @@ function shinyStyle(plugin: PluginContext) {
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
}
} });
}
@@ -44,17 +45,21 @@ 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
resolutionScale: 1,
color: Color(0x000000),
} },
outline: { name: 'on', params: {
scale: 1.0,
threshold: 0.33,
color: Color(0x0000),
} }
includeTransparent: true,
} },
shadow: { name: 'off', params: {} },
}
} });
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-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>
@@ -7,8 +7,9 @@
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
import { CellPack } from '../../extensions/cellpack';
import { DnatcoConfalPyramids } from '../../extensions/dnatco';
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';
@@ -47,18 +48,22 @@ 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';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
const CustomFormats = [
['g3d', G3dProvider] as const
];
const Extensions = {
'volseg': PluginSpec.Behavior(Volseg),
'backgrounds': PluginSpec.Behavior(Backgrounds),
'cellpack': PluginSpec.Behavior(CellPack),
'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
'dnatco-ntcs': PluginSpec.Behavior(DnatcoNtCs),
'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
'rcsb-assembly-symmetry': PluginSpec.Behavior(RCSBAssemblySymmetry),
'rcsb-validation-report': PluginSpec.Behavior(RCSBValidationReport),
@@ -69,6 +74,8 @@ 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),
};
const DefaultViewerOptions = {
@@ -91,6 +98,7 @@ const DefaultViewerOptions = {
enableDpoit: PluginConfig.General.EnableDpoit.defaultValue,
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -104,6 +112,7 @@ const DefaultViewerOptions = {
pdbProvider: PluginConfig.Download.DefaultPdbProvider.defaultValue,
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
volumesAndSegmentationsDefaultServer: VolsegVolumeServerConfig.DefaultServer.defaultValue,
};
type ViewerOptions = typeof DefaultViewerOptions;
@@ -163,6 +172,7 @@ export class Viewer {
[PluginConfig.General.EnableDpoit, o.enableDpoit],
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
[PluginConfig.General.PowerPreference, o.powerPreference],
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
@@ -177,6 +187,7 @@ export class Viewer {
[PluginConfig.Download.DefaultEmdbProvider, o.emdbProvider],
[PluginConfig.Structure.DefaultRepresentationPreset, ViewerAutoPreset.id],
[PluginConfig.Structure.SaccharideCompIdMapType, o.saccharideCompIdMapType],
[VolsegVolumeServerConfig.DefaultServer, o.volumesAndSegmentationsDefaultServer],
]
};
@@ -497,8 +508,14 @@ 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,
};

View File

@@ -63,6 +63,7 @@
var enableDpoit = getParam('enable-dpoit', '[^&]+').trim() === '1';
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
molstar.Viewer.create('app', {
layoutShowControls: !hideControls,
@@ -80,6 +81,7 @@
enableDpoit: enableDpoit ? true : void 0,
preferWebgl1: preferWebgl1,
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
}).then(viewer => {
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);

View File

@@ -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';

View File

@@ -152,7 +152,7 @@ function getImportFrames(d: Data.CifFrame, imports: Imports) {
}
/** get field from given or linked category */
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField|undefined {
function getField(category: string, field: string, d: Data.CifFrame, imports: Imports, ctx: FrameData): Data.CifField | undefined {
const { categories, links } = ctx;
const cat = d.categories[category];
if (cat) {

View File

@@ -25,7 +25,7 @@ async function readFile(path: string) {
}
}
async function parseCif(data: string|Uint8Array) {
async function parseCif(data: string | Uint8Array) {
const comp = CIF.parse(data);
const parsed = await comp.run(p => console.log(Progress.format(p)), 250);
if (parsed.isError) throw parsed;

View File

@@ -38,7 +38,7 @@ function print(volume: Volume) {
}
async function doMesh(volume: Volume, filename: string) {
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, volume, -1, Theme.createEmpty(), { isoValue: Volume.IsoValue.absolute(1.5) })).run();
console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
// Export the mesh in OBJ format.

View File

@@ -25,7 +25,7 @@ import { DemoMoleculeSDF, DemoOrbitals } from './example-data';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
import { setDebugMode, setTimingMode } from '../../mol-util/debug';
import { setDebugMode, setTimingMode, consoleStats } from '../../mol-util/debug';
interface DemoInput {
moleculeSdf: string,
@@ -222,4 +222,5 @@ export class AlphaOrbitalsExample {
(window as any).AlphaOrbitalsExample = new AlphaOrbitalsExample();
(window as any).AlphaOrbitalsExample.setDebugMode = setDebugMode;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;
(window as any).AlphaOrbitalsExample.setTimingMode = setTimingMode;
(window as any).AlphaOrbitalsExample.consoleStats = consoleStats;

View File

@@ -0,0 +1,94 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*
* Example command-line application generating images of PDB structures
* Build: npm install --no-save gl jpeg-js pngjs // these packages are not listed in dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/examples/image-renderer 1cbs ../outputs_1cbs/
*/
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 { ExternalModules, STYLIZED_POSTPROCESSING } from '../../mol-plugin/util/headless-screenshot';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
interface Args {
pdbId: string,
outDirectory: string
}
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Example command-line application generating images of PDB structures' });
parser.add_argument('pdbId', { help: 'PDB identifier' });
parser.add_argument('outDirectory', { help: 'Directory for outputs' });
const args = parser.parse_args();
return { ...args };
}
async function main() {
const args = parseArguments();
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${args.pdbId}.bcif`;
console.log('PDB ID:', args.pdbId);
console.log('Source URL:', url);
console.log('Outputs:', args.outDirectory);
// Create a headless plugin
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
const update = plugin.build();
const structure = update.toRoot()
.apply(Download, { url, isBinary: true })
.apply(ParseCif)
.apply(TrajectoryFromMmCif)
.apply(ModelFromTrajectory)
.apply(StructureFromModel);
const polymer = structure.apply(StructureComponent, { type: { name: 'static', params: 'polymer' } });
const ligand = structure.apply(StructureComponent, { type: { name: 'static', params: 'ligand' } });
polymer.apply(StructureRepresentation3D, {
type: { name: 'cartoon', params: { alpha: 1 } },
colorTheme: { name: 'sequence-id', params: {} },
});
ligand.apply(StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 1 } },
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
sizeTheme: { name: 'physical', params: {} },
});
await update.commit();
// Export images
fs.mkdirSync(args.outDirectory, { recursive: true });
await plugin.saveImage(path.join(args.outDirectory, 'basic.png'));
await plugin.saveImage(path.join(args.outDirectory, 'basic.jpg'));
await plugin.saveImage(path.join(args.outDirectory, 'large.png'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'large.jpg'), { width: 1600, height: 1200 });
await plugin.saveImage(path.join(args.outDirectory, 'stylized.png'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized.jpg'), undefined, STYLIZED_POSTPROCESSING);
await plugin.saveImage(path.join(args.outDirectory, 'stylized-compressed-jpg.jpg'), undefined, STYLIZED_POSTPROCESSING, undefined, 10);
// Export state loadable in Mol* Viewer
await plugin.saveStateSnapshot(path.join(args.outDirectory, 'molstar-state.molj'));
// Cleanup
await plugin.clear();
plugin.dispose();
}
main();

View File

@@ -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>
*/
@@ -24,8 +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) } }
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,
@@ -36,8 +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: {} }
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 +90,8 @@ const Canvas3DPresets = {
canvas3d: <Preset>{
postprocessing: {
occlusion: { name: 'off', params: {} },
outline: { name: 'off', params: {} }
outline: { name: 'off', params: {} },
shadow: { name: 'off', params: {} },
},
renderer: {
ambientIntensity: 0.4,

View File

@@ -52,7 +52,7 @@ export interface Compartment {
}
// Primitives discribing a compartment
export const enum CompartmentPrimitiveType {
export enum CompartmentPrimitiveType {
MetaBall = 0,
Sphere = 1,
Cube = 2,

View File

@@ -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>
@@ -236,7 +236,7 @@ async function getIngredientStructure(plugin: PluginContext, ingredient: Ingredi
structure = await getCurve(name, getCurveTransforms(ingredient), model);
} else {
if ((!results || results.length === 0)) return;
let bu: string|undefined = source.bu ? source.bu : undefined;
let bu: string | undefined = source.bu ? source.bu : undefined;
if (bu) {
if (bu === 'AU') {
bu = undefined;
@@ -585,7 +585,7 @@ export const LoadCellPackModel = StateAction.build({
... ctx.managers.structure.component.state.options,
visualQuality: 'custom',
ignoreLight: true,
showHydrogens: false,
hydrogens: 'hide-all',
});
ctx.canvas3d?.setProps({
multiSample: { mode: 'off' },
@@ -600,10 +600,21 @@ 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: {
name: 'on',
params: {
bias: 0.6,
maxDistance: 80,
steps: 3,
tolerance: 1.0,
}
},
outline: {
@@ -612,6 +623,7 @@ export const LoadCellPackModel = StateAction.build({
scale: 1,
threshold: 0.33,
color: ColorNames.black,
includeTransparent: true,
}
}
}

View File

@@ -0,0 +1,59 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { ConfalPyramidsPreset } from './confal-pyramids/behavior';
import { ConfalPyramidsColorThemeProvider } from './confal-pyramids/color';
import { ConfalPyramidsProvider } from './confal-pyramids/property';
import { ConfalPyramidsRepresentationProvider } from './confal-pyramids/representation';
import { NtCTubePreset } from './ntc-tube/behavior';
import { NtCTubeColorThemeProvider } from './ntc-tube/color';
import { NtCTubeProvider } from './ntc-tube/property';
import { NtCTubeRepresentationProvider } from './ntc-tube/representation';
export const DnatcoNtCs = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-ntcs',
category: 'custom-props',
display: {
name: 'DNATCO NtC Annotations',
description: 'DNATCO NtC Annotations',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
register(): void {
this.ctx.customModelProperties.register(ConfalPyramidsProvider, this.params.autoAttach);
this.ctx.customModelProperties.register(NtCTubeProvider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.add(NtCTubeColorThemeProvider);
this.ctx.representation.structure.registry.add(NtCTubeRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.registerPreset(NtCTubePreset);
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.customModelProperties.unregister(NtCTubeProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.remove(NtCTubeRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(NtCTubeColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(ConfalPyramidsPreset);
this.ctx.builders.structure.representation.unregisterPreset(NtCTubePreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});

View File

@@ -0,0 +1,219 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { Color, ColorMap } from '../../mol-util/color';
export const DefaultNtCClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
export const ErrorColor = Color(0xFFA10A);
export const NtCColors = ColorMap({
NANT_Upr: DefaultNtCClassColors.N,
NANT_Lwr: DefaultNtCClassColors.N,
AA00_Upr: DefaultNtCClassColors.A,
AA00_Lwr: DefaultNtCClassColors.A,
AA02_Upr: DefaultNtCClassColors.A,
AA02_Lwr: DefaultNtCClassColors.A,
AA03_Upr: DefaultNtCClassColors.A,
AA03_Lwr: DefaultNtCClassColors.A,
AA04_Upr: DefaultNtCClassColors.A,
AA04_Lwr: DefaultNtCClassColors.A,
AA08_Upr: DefaultNtCClassColors.A,
AA08_Lwr: DefaultNtCClassColors.A,
AA09_Upr: DefaultNtCClassColors.A,
AA09_Lwr: DefaultNtCClassColors.A,
AA01_Upr: DefaultNtCClassColors.A,
AA01_Lwr: DefaultNtCClassColors.A,
AA05_Upr: DefaultNtCClassColors.A,
AA05_Lwr: DefaultNtCClassColors.A,
AA06_Upr: DefaultNtCClassColors.A,
AA06_Lwr: DefaultNtCClassColors.A,
AA10_Upr: DefaultNtCClassColors.A,
AA10_Lwr: DefaultNtCClassColors.A,
AA11_Upr: DefaultNtCClassColors.A,
AA11_Lwr: DefaultNtCClassColors.A,
AA07_Upr: DefaultNtCClassColors.A,
AA07_Lwr: DefaultNtCClassColors.A,
AA12_Upr: DefaultNtCClassColors.A,
AA12_Lwr: DefaultNtCClassColors.A,
AA13_Upr: DefaultNtCClassColors.A,
AA13_Lwr: DefaultNtCClassColors.A,
AB01_Upr: DefaultNtCClassColors.A,
AB01_Lwr: DefaultNtCClassColors.B,
AB02_Upr: DefaultNtCClassColors.A,
AB02_Lwr: DefaultNtCClassColors.B,
AB03_Upr: DefaultNtCClassColors.A,
AB03_Lwr: DefaultNtCClassColors.B,
AB04_Upr: DefaultNtCClassColors.A,
AB04_Lwr: DefaultNtCClassColors.B,
AB05_Upr: DefaultNtCClassColors.A,
AB05_Lwr: DefaultNtCClassColors.B,
BA01_Upr: DefaultNtCClassColors.B,
BA01_Lwr: DefaultNtCClassColors.A,
BA05_Upr: DefaultNtCClassColors.B,
BA05_Lwr: DefaultNtCClassColors.A,
BA09_Upr: DefaultNtCClassColors.B,
BA09_Lwr: DefaultNtCClassColors.A,
BA08_Upr: DefaultNtCClassColors.BII,
BA08_Lwr: DefaultNtCClassColors.A,
BA10_Upr: DefaultNtCClassColors.B,
BA10_Lwr: DefaultNtCClassColors.A,
BA13_Upr: DefaultNtCClassColors.BII,
BA13_Lwr: DefaultNtCClassColors.A,
BA16_Upr: DefaultNtCClassColors.BII,
BA16_Lwr: DefaultNtCClassColors.A,
BA17_Upr: DefaultNtCClassColors.BII,
BA17_Lwr: DefaultNtCClassColors.A,
BB00_Upr: DefaultNtCClassColors.B,
BB00_Lwr: DefaultNtCClassColors.B,
BB01_Upr: DefaultNtCClassColors.B,
BB01_Lwr: DefaultNtCClassColors.B,
BB17_Upr: DefaultNtCClassColors.B,
BB17_Lwr: DefaultNtCClassColors.B,
BB02_Upr: DefaultNtCClassColors.B,
BB02_Lwr: DefaultNtCClassColors.B,
BB03_Upr: DefaultNtCClassColors.B,
BB03_Lwr: DefaultNtCClassColors.B,
BB11_Upr: DefaultNtCClassColors.B,
BB11_Lwr: DefaultNtCClassColors.B,
BB16_Upr: DefaultNtCClassColors.B,
BB16_Lwr: DefaultNtCClassColors.B,
BB04_Upr: DefaultNtCClassColors.B,
BB04_Lwr: DefaultNtCClassColors.BII,
BB05_Upr: DefaultNtCClassColors.B,
BB05_Lwr: DefaultNtCClassColors.BII,
BB07_Upr: DefaultNtCClassColors.BII,
BB07_Lwr: DefaultNtCClassColors.BII,
BB08_Upr: DefaultNtCClassColors.BII,
BB08_Lwr: DefaultNtCClassColors.BII,
BB10_Upr: DefaultNtCClassColors.miB,
BB10_Lwr: DefaultNtCClassColors.miB,
BB12_Upr: DefaultNtCClassColors.miB,
BB12_Lwr: DefaultNtCClassColors.miB,
BB13_Upr: DefaultNtCClassColors.miB,
BB13_Lwr: DefaultNtCClassColors.miB,
BB14_Upr: DefaultNtCClassColors.miB,
BB14_Lwr: DefaultNtCClassColors.miB,
BB15_Upr: DefaultNtCClassColors.miB,
BB15_Lwr: DefaultNtCClassColors.miB,
BB20_Upr: DefaultNtCClassColors.miB,
BB20_Lwr: DefaultNtCClassColors.miB,
IC01_Upr: DefaultNtCClassColors.IC,
IC01_Lwr: DefaultNtCClassColors.IC,
IC02_Upr: DefaultNtCClassColors.IC,
IC02_Lwr: DefaultNtCClassColors.IC,
IC03_Upr: DefaultNtCClassColors.IC,
IC03_Lwr: DefaultNtCClassColors.IC,
IC04_Upr: DefaultNtCClassColors.IC,
IC04_Lwr: DefaultNtCClassColors.IC,
IC05_Upr: DefaultNtCClassColors.IC,
IC05_Lwr: DefaultNtCClassColors.IC,
IC06_Upr: DefaultNtCClassColors.IC,
IC06_Lwr: DefaultNtCClassColors.IC,
IC07_Upr: DefaultNtCClassColors.IC,
IC07_Lwr: DefaultNtCClassColors.IC,
OP01_Upr: DefaultNtCClassColors.OPN,
OP01_Lwr: DefaultNtCClassColors.OPN,
OP02_Upr: DefaultNtCClassColors.OPN,
OP02_Lwr: DefaultNtCClassColors.OPN,
OP03_Upr: DefaultNtCClassColors.OPN,
OP03_Lwr: DefaultNtCClassColors.OPN,
OP04_Upr: DefaultNtCClassColors.OPN,
OP04_Lwr: DefaultNtCClassColors.OPN,
OP05_Upr: DefaultNtCClassColors.OPN,
OP05_Lwr: DefaultNtCClassColors.OPN,
OP06_Upr: DefaultNtCClassColors.OPN,
OP06_Lwr: DefaultNtCClassColors.OPN,
OP07_Upr: DefaultNtCClassColors.OPN,
OP07_Lwr: DefaultNtCClassColors.OPN,
OP08_Upr: DefaultNtCClassColors.OPN,
OP08_Lwr: DefaultNtCClassColors.OPN,
OP09_Upr: DefaultNtCClassColors.OPN,
OP09_Lwr: DefaultNtCClassColors.OPN,
OP10_Upr: DefaultNtCClassColors.OPN,
OP10_Lwr: DefaultNtCClassColors.OPN,
OP11_Upr: DefaultNtCClassColors.OPN,
OP11_Lwr: DefaultNtCClassColors.OPN,
OP12_Upr: DefaultNtCClassColors.OPN,
OP12_Lwr: DefaultNtCClassColors.OPN,
OP13_Upr: DefaultNtCClassColors.OPN,
OP13_Lwr: DefaultNtCClassColors.OPN,
OP14_Upr: DefaultNtCClassColors.OPN,
OP14_Lwr: DefaultNtCClassColors.OPN,
OP15_Upr: DefaultNtCClassColors.OPN,
OP15_Lwr: DefaultNtCClassColors.OPN,
OP16_Upr: DefaultNtCClassColors.OPN,
OP16_Lwr: DefaultNtCClassColors.OPN,
OP17_Upr: DefaultNtCClassColors.OPN,
OP17_Lwr: DefaultNtCClassColors.OPN,
OP18_Upr: DefaultNtCClassColors.OPN,
OP18_Lwr: DefaultNtCClassColors.OPN,
OP19_Upr: DefaultNtCClassColors.OPN,
OP19_Lwr: DefaultNtCClassColors.OPN,
OP20_Upr: DefaultNtCClassColors.OPN,
OP20_Lwr: DefaultNtCClassColors.OPN,
OP21_Upr: DefaultNtCClassColors.OPN,
OP21_Lwr: DefaultNtCClassColors.OPN,
OP22_Upr: DefaultNtCClassColors.OPN,
OP22_Lwr: DefaultNtCClassColors.OPN,
OP23_Upr: DefaultNtCClassColors.OPN,
OP23_Lwr: DefaultNtCClassColors.OPN,
OP24_Upr: DefaultNtCClassColors.OPN,
OP24_Lwr: DefaultNtCClassColors.OPN,
OP25_Upr: DefaultNtCClassColors.OPN,
OP25_Lwr: DefaultNtCClassColors.OPN,
OP26_Upr: DefaultNtCClassColors.OPN,
OP26_Lwr: DefaultNtCClassColors.OPN,
OP27_Upr: DefaultNtCClassColors.OPN,
OP27_Lwr: DefaultNtCClassColors.OPN,
OP28_Upr: DefaultNtCClassColors.OPN,
OP28_Lwr: DefaultNtCClassColors.OPN,
OP29_Upr: DefaultNtCClassColors.OPN,
OP29_Lwr: DefaultNtCClassColors.OPN,
OP30_Upr: DefaultNtCClassColors.OPN,
OP30_Lwr: DefaultNtCClassColors.OPN,
OP31_Upr: DefaultNtCClassColors.OPN,
OP31_Lwr: DefaultNtCClassColors.OPN,
OPS1_Upr: DefaultNtCClassColors.OPN,
OPS1_Lwr: DefaultNtCClassColors.OPN,
OP1S_Upr: DefaultNtCClassColors.OPN,
OP1S_Lwr: DefaultNtCClassColors.SYN,
AAS1_Upr: DefaultNtCClassColors.SYN,
AAS1_Lwr: DefaultNtCClassColors.A,
AB1S_Upr: DefaultNtCClassColors.A,
AB1S_Lwr: DefaultNtCClassColors.SYN,
AB2S_Upr: DefaultNtCClassColors.A,
AB2S_Lwr: DefaultNtCClassColors.SYN,
BB1S_Upr: DefaultNtCClassColors.B,
BB1S_Lwr: DefaultNtCClassColors.SYN,
BB2S_Upr: DefaultNtCClassColors.B,
BB2S_Lwr: DefaultNtCClassColors.SYN,
BBS1_Upr: DefaultNtCClassColors.SYN,
BBS1_Lwr: DefaultNtCClassColors.B,
ZZ01_Upr: DefaultNtCClassColors.Z,
ZZ01_Lwr: DefaultNtCClassColors.Z,
ZZ02_Upr: DefaultNtCClassColors.Z,
ZZ02_Lwr: DefaultNtCClassColors.Z,
ZZ1S_Upr: DefaultNtCClassColors.Z,
ZZ1S_Lwr: DefaultNtCClassColors.SYN,
ZZ2S_Upr: DefaultNtCClassColors.Z,
ZZ2S_Lwr: DefaultNtCClassColors.SYN,
ZZS1_Upr: DefaultNtCClassColors.SYN,
ZZS1_Lwr: DefaultNtCClassColors.Z,
ZZS2_Upr: DefaultNtCClassColors.SYN,
ZZS2_Lwr: DefaultNtCClassColors.Z,
});

View File

@@ -6,23 +6,22 @@
*/
import { ConfalPyramidsColorThemeProvider } from './color';
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramidsRepresentationProvider } from './representation';
import { ConfalPyramidsTypes } from './types';
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider({
export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-confal-pyramids',
display: {
name: 'Confal Pyramids', group: 'Annotation',
description: 'Schematic depiction of conformer class and confal value.',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => ConfalPyramids.isApplicable(m));
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
@@ -48,50 +47,7 @@ export const DnatcoConfalPyramidsPreset = StructureRepresentationPresetProvider(
}
});
export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean, showToolTip: boolean }>({
name: 'dnatco-confal-pyramids-prop',
category: 'custom-props',
display: {
name: 'Confal Pyramids',
description: 'Schematic depiction of conformer class and confal value.',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
private provider = ConfalPyramidsProvider;
register(): void {
this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
this.ctx.builders.structure.representation.registerPreset(DnatcoConfalPyramidsPreset);
}
update(p: { autoAttach: boolean, showToolTip: boolean }) {
const updated = this.params.autoAttach !== p.autoAttach;
this.params.autoAttach = p.autoAttach;
this.params.showToolTip = p.showToolTip;
this.ctx.customModelProperties.setDefaultAutoAttach(this.provider.descriptor.name, this.params.autoAttach);
return updated;
}
unregister() {
this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
this.ctx.builders.structure.representation.unregisterPreset(DnatcoConfalPyramidsPreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
showToolTip: PD.Boolean(true)
})
});
export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
const { step } = halfPyramid;
export function confalPyramidLabel(step: DnatcoTypes.Step) {
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}

View File

@@ -5,8 +5,10 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ErrorColor, NtCColors } from '../color';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
@@ -19,215 +21,7 @@ import { ObjectKeys } from '../../../mol-util/type-helpers';
const Description = 'Assigns colors to confal pyramids';
const DefaultClassColors = {
A: 0xFFC1C1,
B: 0xC8CFFF,
BII: 0x0059DA,
miB: 0x3BE8FB,
Z: 0x01F60E,
IC: 0xFA5CFB,
OPN: 0xE90000,
SYN: 0xFFFF01,
N: 0xF2F2F2,
};
const ErrorColor = Color(0xFFA10A);
const PyramidsColors = ColorMap({
NANT_Upr: DefaultClassColors.N,
NANT_Lwr: DefaultClassColors.N,
AA00_Upr: DefaultClassColors.A,
AA00_Lwr: DefaultClassColors.A,
AA02_Upr: DefaultClassColors.A,
AA02_Lwr: DefaultClassColors.A,
AA03_Upr: DefaultClassColors.A,
AA03_Lwr: DefaultClassColors.A,
AA04_Upr: DefaultClassColors.A,
AA04_Lwr: DefaultClassColors.A,
AA08_Upr: DefaultClassColors.A,
AA08_Lwr: DefaultClassColors.A,
AA09_Upr: DefaultClassColors.A,
AA09_Lwr: DefaultClassColors.A,
AA01_Upr: DefaultClassColors.A,
AA01_Lwr: DefaultClassColors.A,
AA05_Upr: DefaultClassColors.A,
AA05_Lwr: DefaultClassColors.A,
AA06_Upr: DefaultClassColors.A,
AA06_Lwr: DefaultClassColors.A,
AA10_Upr: DefaultClassColors.A,
AA10_Lwr: DefaultClassColors.A,
AA11_Upr: DefaultClassColors.A,
AA11_Lwr: DefaultClassColors.A,
AA07_Upr: DefaultClassColors.A,
AA07_Lwr: DefaultClassColors.A,
AA12_Upr: DefaultClassColors.A,
AA12_Lwr: DefaultClassColors.A,
AA13_Upr: DefaultClassColors.A,
AA13_Lwr: DefaultClassColors.A,
AB01_Upr: DefaultClassColors.A,
AB01_Lwr: DefaultClassColors.B,
AB02_Upr: DefaultClassColors.A,
AB02_Lwr: DefaultClassColors.B,
AB03_Upr: DefaultClassColors.A,
AB03_Lwr: DefaultClassColors.B,
AB04_Upr: DefaultClassColors.A,
AB04_Lwr: DefaultClassColors.B,
AB05_Upr: DefaultClassColors.A,
AB05_Lwr: DefaultClassColors.B,
BA01_Upr: DefaultClassColors.B,
BA01_Lwr: DefaultClassColors.A,
BA05_Upr: DefaultClassColors.B,
BA05_Lwr: DefaultClassColors.A,
BA09_Upr: DefaultClassColors.B,
BA09_Lwr: DefaultClassColors.A,
BA08_Upr: DefaultClassColors.BII,
BA08_Lwr: DefaultClassColors.A,
BA10_Upr: DefaultClassColors.B,
BA10_Lwr: DefaultClassColors.A,
BA13_Upr: DefaultClassColors.BII,
BA13_Lwr: DefaultClassColors.A,
BA16_Upr: DefaultClassColors.BII,
BA16_Lwr: DefaultClassColors.A,
BA17_Upr: DefaultClassColors.BII,
BA17_Lwr: DefaultClassColors.A,
BB00_Upr: DefaultClassColors.B,
BB00_Lwr: DefaultClassColors.B,
BB01_Upr: DefaultClassColors.B,
BB01_Lwr: DefaultClassColors.B,
BB17_Upr: DefaultClassColors.B,
BB17_Lwr: DefaultClassColors.B,
BB02_Upr: DefaultClassColors.B,
BB02_Lwr: DefaultClassColors.B,
BB03_Upr: DefaultClassColors.B,
BB03_Lwr: DefaultClassColors.B,
BB11_Upr: DefaultClassColors.B,
BB11_Lwr: DefaultClassColors.B,
BB16_Upr: DefaultClassColors.B,
BB16_Lwr: DefaultClassColors.B,
BB04_Upr: DefaultClassColors.B,
BB04_Lwr: DefaultClassColors.BII,
BB05_Upr: DefaultClassColors.B,
BB05_Lwr: DefaultClassColors.BII,
BB07_Upr: DefaultClassColors.BII,
BB07_Lwr: DefaultClassColors.BII,
BB08_Upr: DefaultClassColors.BII,
BB08_Lwr: DefaultClassColors.BII,
BB10_Upr: DefaultClassColors.miB,
BB10_Lwr: DefaultClassColors.miB,
BB12_Upr: DefaultClassColors.miB,
BB12_Lwr: DefaultClassColors.miB,
BB13_Upr: DefaultClassColors.miB,
BB13_Lwr: DefaultClassColors.miB,
BB14_Upr: DefaultClassColors.miB,
BB14_Lwr: DefaultClassColors.miB,
BB15_Upr: DefaultClassColors.miB,
BB15_Lwr: DefaultClassColors.miB,
BB20_Upr: DefaultClassColors.miB,
BB20_Lwr: DefaultClassColors.miB,
IC01_Upr: DefaultClassColors.IC,
IC01_Lwr: DefaultClassColors.IC,
IC02_Upr: DefaultClassColors.IC,
IC02_Lwr: DefaultClassColors.IC,
IC03_Upr: DefaultClassColors.IC,
IC03_Lwr: DefaultClassColors.IC,
IC04_Upr: DefaultClassColors.IC,
IC04_Lwr: DefaultClassColors.IC,
IC05_Upr: DefaultClassColors.IC,
IC05_Lwr: DefaultClassColors.IC,
IC06_Upr: DefaultClassColors.IC,
IC06_Lwr: DefaultClassColors.IC,
IC07_Upr: DefaultClassColors.IC,
IC07_Lwr: DefaultClassColors.IC,
OP01_Upr: DefaultClassColors.OPN,
OP01_Lwr: DefaultClassColors.OPN,
OP02_Upr: DefaultClassColors.OPN,
OP02_Lwr: DefaultClassColors.OPN,
OP03_Upr: DefaultClassColors.OPN,
OP03_Lwr: DefaultClassColors.OPN,
OP04_Upr: DefaultClassColors.OPN,
OP04_Lwr: DefaultClassColors.OPN,
OP05_Upr: DefaultClassColors.OPN,
OP05_Lwr: DefaultClassColors.OPN,
OP06_Upr: DefaultClassColors.OPN,
OP06_Lwr: DefaultClassColors.OPN,
OP07_Upr: DefaultClassColors.OPN,
OP07_Lwr: DefaultClassColors.OPN,
OP08_Upr: DefaultClassColors.OPN,
OP08_Lwr: DefaultClassColors.OPN,
OP09_Upr: DefaultClassColors.OPN,
OP09_Lwr: DefaultClassColors.OPN,
OP10_Upr: DefaultClassColors.OPN,
OP10_Lwr: DefaultClassColors.OPN,
OP11_Upr: DefaultClassColors.OPN,
OP11_Lwr: DefaultClassColors.OPN,
OP12_Upr: DefaultClassColors.OPN,
OP12_Lwr: DefaultClassColors.OPN,
OP13_Upr: DefaultClassColors.OPN,
OP13_Lwr: DefaultClassColors.OPN,
OP14_Upr: DefaultClassColors.OPN,
OP14_Lwr: DefaultClassColors.OPN,
OP15_Upr: DefaultClassColors.OPN,
OP15_Lwr: DefaultClassColors.OPN,
OP16_Upr: DefaultClassColors.OPN,
OP16_Lwr: DefaultClassColors.OPN,
OP17_Upr: DefaultClassColors.OPN,
OP17_Lwr: DefaultClassColors.OPN,
OP18_Upr: DefaultClassColors.OPN,
OP18_Lwr: DefaultClassColors.OPN,
OP19_Upr: DefaultClassColors.OPN,
OP19_Lwr: DefaultClassColors.OPN,
OP20_Upr: DefaultClassColors.OPN,
OP20_Lwr: DefaultClassColors.OPN,
OP21_Upr: DefaultClassColors.OPN,
OP21_Lwr: DefaultClassColors.OPN,
OP22_Upr: DefaultClassColors.OPN,
OP22_Lwr: DefaultClassColors.OPN,
OP23_Upr: DefaultClassColors.OPN,
OP23_Lwr: DefaultClassColors.OPN,
OP24_Upr: DefaultClassColors.OPN,
OP24_Lwr: DefaultClassColors.OPN,
OP25_Upr: DefaultClassColors.OPN,
OP25_Lwr: DefaultClassColors.OPN,
OP26_Upr: DefaultClassColors.OPN,
OP26_Lwr: DefaultClassColors.OPN,
OP27_Upr: DefaultClassColors.OPN,
OP27_Lwr: DefaultClassColors.OPN,
OP28_Upr: DefaultClassColors.OPN,
OP28_Lwr: DefaultClassColors.OPN,
OP29_Upr: DefaultClassColors.OPN,
OP29_Lwr: DefaultClassColors.OPN,
OP30_Upr: DefaultClassColors.OPN,
OP30_Lwr: DefaultClassColors.OPN,
OP31_Upr: DefaultClassColors.OPN,
OP31_Lwr: DefaultClassColors.OPN,
OPS1_Upr: DefaultClassColors.OPN,
OPS1_Lwr: DefaultClassColors.OPN,
OP1S_Upr: DefaultClassColors.OPN,
OP1S_Lwr: DefaultClassColors.SYN,
AAS1_Upr: DefaultClassColors.SYN,
AAS1_Lwr: DefaultClassColors.A,
AB1S_Upr: DefaultClassColors.A,
AB1S_Lwr: DefaultClassColors.SYN,
AB2S_Upr: DefaultClassColors.A,
AB2S_Lwr: DefaultClassColors.SYN,
BB1S_Upr: DefaultClassColors.B,
BB1S_Lwr: DefaultClassColors.SYN,
BB2S_Upr: DefaultClassColors.B,
BB2S_Lwr: DefaultClassColors.SYN,
BBS1_Upr: DefaultClassColors.SYN,
BBS1_Lwr: DefaultClassColors.B,
ZZ01_Upr: DefaultClassColors.Z,
ZZ01_Lwr: DefaultClassColors.Z,
ZZ02_Upr: DefaultClassColors.Z,
ZZ02_Lwr: DefaultClassColors.Z,
ZZ1S_Upr: DefaultClassColors.Z,
ZZ1S_Lwr: DefaultClassColors.SYN,
ZZ2S_Upr: DefaultClassColors.Z,
ZZ2S_Lwr: DefaultClassColors.SYN,
ZZS1_Upr: DefaultClassColors.SYN,
ZZS1_Lwr: DefaultClassColors.Z,
ZZS2_Upr: DefaultClassColors.SYN,
ZZS2_Lwr: DefaultClassColors.Z,
});
const PyramidsColors = ColorMap({ ...NtCColors });
type PyramidsColors = typeof PyramidsColors;
export const ConfalPyramidsColorThemeParams = {
@@ -272,7 +66,7 @@ export const ConfalPyramidsColorThemeProvider: ColorTheme.Provider<ConfalPyramid
factory: ConfalPyramidsColorTheme,
getParams: getConfalPyramidsColorThemeParams,
defaultValues: PD.getDefaultValues(ConfalPyramidsColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => ConfalPyramids.isApplicable(m)),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? ConfalPyramidsProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && ConfalPyramidsProvider.ref(data.structure.models[0], false)

View File

@@ -5,90 +5,18 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsTypes as CPT } from './types';
import { Column, Table } from '../../../mol-data/db';
import { toTable } from '../../../mol-io/reader/cif/schema';
import { Dnatco, DnatcoParams, DnatcoSteps } from '../property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
export namespace ConfalPyramids {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: ConfalPyramidsProps): Promise<CustomProperty.Data<ConfalPyramids>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = createPyramidsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
export const ConfalPyramidsParams = {};
export const ConfalPyramidsParams = { ...DnatcoParams };
export type ConfalPyramidsParams = typeof ConfalPyramidsParams;
export type ConfalPyramidsProps = PD.Values<ConfalPyramidsParams>;
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, ConfalPyramids> = CustomModelProperty.createProvider({
export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramidsParams, DnatcoSteps> = CustomModelProperty.createProvider({
label: 'Confal Pyramids',
descriptor: CustomPropertyDescriptor({
name: 'confal_pyramids',
@@ -96,94 +24,9 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
type: 'static',
defaultParams: ConfalPyramidsParams,
getParams: (data: Model) => ConfalPyramidsParams,
isApplicable: (data: Model) => ConfalPyramids.isApplicable(data),
isApplicable: (data: Model) => Dnatco.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<ConfalPyramidsProps>) => {
const p = { ...PD.getDefaultValues(ConfalPyramidsParams), ...props };
return ConfalPyramids.fromCif(ctx, data, p);
return Dnatco.fromCif(ctx, data, p);
}
});
type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
function createPyramidsFromCif(
model: Model,
cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): CPT.Steps {
const steps = new Array<CPT.Step>();
const mapping = new Array<CPT.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -5,9 +5,10 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramids, ConfalPyramidsProvider } from './property';
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramidsIterator } from './util';
import { ConfalPyramidsTypes as CPT } from './types';
import { Dnatco } from '../property';
import { Interval } from '../../../mol-data/int';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
@@ -87,6 +88,8 @@ function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Str
const it = new ConfalPyramidsIterator(structure, unit);
while (it.hasNext) {
const allPoints = it.move();
if (!allPoints)
continue;
for (const points of allPoints) {
const { O3, P, OP1, OP2, O5, confalScore } = points;
@@ -150,9 +153,7 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
if (halfPyramidsCount <= groupId) return EmptyLoci;
const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
const step = data.steps[idx];
return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
return CPT.Loci(data.steps, [idx]);
}
function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
@@ -197,7 +198,7 @@ export const ConfalPyramidsRepresentationProvider = StructureRepresentationProvi
defaultValues: PD.getDefaultValues(ConfalPyramidsParams),
defaultColorTheme: { name: 'confal-pyramids' },
defaultSizeTheme: { name: 'uniform' },
isApplicable: (structure: Structure) => structure.models.some(m => ConfalPyramids.isApplicable(m)),
isApplicable: (structure: Structure) => structure.models.some(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, structure: Structure) => ConfalPyramidsProvider.attach(ctx, structure.model, void 0, true),
detach: (data) => ConfalPyramidsProvider.ref(data.model, false),

View File

@@ -5,61 +5,29 @@
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from '../types';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
import { confalPyramidLabel } from './behavior';
export namespace ConfalPyramidsTypes {
export const DataTag = 'dnatco-confal-half-pyramid';
export interface Location extends DataLocation<DnatcoTypes.HalfStep, {}> {}
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfPyramid {
step: Step,
isLower: boolean,
}
export interface Location extends DataLocation<HalfPyramid, {}> {}
export function Location(step: Step, isLower: boolean) {
return DataLocation(DataTag, { step, isLower }, {});
export function Location(step: DnatcoTypes.Step, isLower: boolean) {
return DataLocation(DnatcoTypes.DataTag, { step, isLower }, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DataTag;
return !!x && x.kind === 'data-location' && x.tag === DnatcoTypes.DataTag;
}
export interface Loci extends DataLoci<HalfPyramid, {}> {}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
export function Loci(data: DnatcoTypes.Step[], elements: ReadonlyArray<number>): Loci {
return DataLoci(DnatcoTypes.DataTag, data, elements, undefined, () => elements[0] !== undefined ? confalPyramidLabel(data[elements[0]]) : '');
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
return !!x && x.kind === 'data-loci' && x.tag === DnatcoTypes.DataTag;
}
}

View File

@@ -1,16 +1,15 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ConfalPyramidsProvider } from './property';
import { ConfalPyramidsTypes as CPT } from './types';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { Segmentation } from '../../../mol-data/int';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
type Residue = Segmentation.Segment<ResidueIndex>;
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
export type Pyramid = {
O3: ElementIndex,
@@ -22,31 +21,17 @@ export type Pyramid = {
stepIdx: number,
};
const EmptyStepIndices = new Array<number>();
function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
return loc.element;
}
return -1 as ElementIndex;
}
function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
const P = getAtomIndex(loc, two, ['P'], altIdTwo);
const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
function getPyramid(
loc: StructureElement.Location,
one: DnatcoUtil.Residue, two: DnatcoUtil.Residue,
altIdOne: string, altIdTwo: string,
insCodeOne: string, insCodeTwo: string,
confalScore: number, stepIdx: number): Pyramid {
const O3 = DnatcoUtil.getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne, insCodeOne);
const P = DnatcoUtil.getAtomIndex(loc, two, ['P'], altIdTwo, insCodeTwo);
const OP1 = DnatcoUtil.getAtomIndex(loc, two, ['OP1'], altIdTwo, insCodeTwo);
const OP2 = DnatcoUtil.getAtomIndex(loc, two, ['OP2'], altIdTwo, insCodeTwo);
const O5 = DnatcoUtil.getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo, insCodeTwo);
return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
}
@@ -54,39 +39,29 @@ function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue,
export class ConfalPyramidsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
private residueOne?: Residue;
private residueTwo: Residue;
private data?: CPT.Steps;
private residueOne?: DnatcoUtil.Residue;
private residueTwo: DnatcoUtil.Residue;
private data?: DnatcoTypes.Steps;
private loc: StructureElement.Location;
private getStepIndices(r: Residue) {
this.loc.element = this.loc.unit.elements[r.start];
const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(this.loc);
const seqId = StructureProperties.residue.auth_seq_id(this.loc);
const chains = this.data!.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
return residues.get(seqId) ?? EmptyStepIndices;
}
private moveStep() {
this.residueOne = copyResidue(this.residueTwo);
this.residueTwo = copyResidue(this.residueIt.move())!;
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
// Check for discontinuity
if (this.residueTwo.index !== (this.residueOne!.index + 1))
return void 0;
return this.toPyramids(this.residueOne!, this.residueTwo);
}
private toPyramids(one: Residue, two: Residue) {
const indices = this.getStepIndices(one);
private toPyramids(one: DnatcoUtil.Residue, two: DnatcoUtil.Residue) {
const indices = DnatcoUtil.getStepIndices(this.data!, this.loc, one);
const points = [];
for (const idx of indices) {
const step = this.data!.steps[idx];
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.PDB_ins_code_1, step.PDB_ins_code_2, step.confal_score, idx));
}
return points;
@@ -121,6 +96,8 @@ export class ConfalPyramidsIterator {
return this.moveStep();
} else {
this.residueIt.setSegment(this.chainIt.move());
if (this.residueIt.hasNext)
this.residueTwo = this.residueIt.move();
return this.moveStep();
}
}

View File

@@ -1,8 +1,8 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export { DnatcoConfalPyramids } from './confal-pyramids/behavior';
export { DnatcoNtCs } from './behavior';

View File

@@ -0,0 +1,57 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeColorThemeProvider } from './color';
import { NtCTubeProvider } from './property';
import { NtCTubeRepresentationProvider } from './representation';
import { DnatcoTypes } from '../types';
import { Dnatco } from '../property';
import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { Task } from '../../../mol-task';
export const NtCTubePreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-ntc-tube',
display: {
name: 'NtC Tube', group: 'Annotation',
description: 'NtC Tube',
},
isApplicable(a) {
return a.data.models.length >= 1 && a.data.models.some(m => Dnatco.isApplicable(m));
},
params: () => StructureRepresentationPresetProvider.CommonParams,
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const model = structureCell?.obj?.data.model;
if (!structureCell || !model) return {};
await plugin.runTask(Task.create('NtC tube', async runtime => {
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
}));
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
const tube = await plugin.builders.structure.tryCreateComponentStatic(structureCell, 'nucleic', { label: 'NtC Tube' });
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
let tubeRepr;
if (representations)
tubeRepr = builder.buildRepresentation(update, tube, { type: NtCTubeRepresentationProvider, typeParams, color: NtCTubeColorThemeProvider }, { tag: 'ntc-tube' });
await update.commit({ revertOnError: true });
return { components: { ...components, tube }, representations: { ...representations, tubeRepr } };
}
});
export function NtCTubeSegmentLabel(step: DnatcoTypes.Step) {
return `
<b>${step.auth_asym_id_1}</b> |
<b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
<i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
`;
}

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { ErrorColor, NtCColors } from '../color';
import { NtCTubeProvider } from './property';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { Location } from '../../../mol-model/location';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { ColorTheme } from '../../../mol-theme/color';
import { ThemeDataContext } from '../../../mol-theme/theme';
import { Color, ColorMap } from '../../../mol-util/color';
import { getColorMapParams } from '../../../mol-util/color/params';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TableLegend } from '../../../mol-util/legend';
import { ObjectKeys } from '../../../mol-util/type-helpers';
const Description = 'Assigns colors to NtC Tube segments';
const NtCTubeColors = ColorMap({
...NtCColors,
residueMarker: Color(0x222222),
stepBoundaryMarker: Color(0x656565),
});
type NtCTubeColors = typeof NtCTubeColors;
export const NtCTubeColorThemeParams = {
colors: PD.MappedStatic('default', {
'default': PD.EmptyGroup(),
'custom': PD.Group(getColorMapParams(NtCTubeColors)),
'uniform': PD.Color(Color(0xEEEEEE)),
}),
markResidueBoundaries: PD.Boolean(true),
markSegmentBoundaries: PD.Boolean(true),
};
export type NtCTubeColorThemeParams = typeof NtCTubeColorThemeParams;
export function getNtCTubeColorThemeParams(ctx: ThemeDataContext) {
return PD.clone(NtCTubeColorThemeParams);
}
export function NtCTubeColorTheme(ctx: ThemeDataContext, props: PD.Values<NtCTubeColorThemeParams>): ColorTheme<NtCTubeColorThemeParams> {
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)) {
const { data } = location;
const { step, kind } = data;
let key;
if (kind === 'upper')
key = step.NtC + '_Upr' as keyof NtCTubeColors;
else if (kind === 'lower')
key = step.NtC + '_Lwr' as keyof NtCTubeColors;
else if (kind === 'residue-boundary')
key = (!props.markResidueBoundaries ? step.NtC + '_Lwr' : 'residueMarker') as keyof NtCTubeColors;
else /* segment-boundary */
key = (!props.markSegmentBoundaries ? step.NtC + '_Lwr' : 'stepBoundaryMarker') as keyof NtCTubeColors;
return colorMap[key] ?? ErrorColor;
}
return ErrorColor;
}
return {
factory: NtCTubeColorTheme,
granularity: 'group',
color,
props,
description: Description,
legend: TableLegend(ObjectKeys(colorMap).map(k => [k.replace('_', ' '), colorMap[k]] as [string, Color]).concat([['Error', ErrorColor]])),
};
}
export const NtCTubeColorThemeProvider: ColorTheme.Provider<NtCTubeColorThemeParams, 'ntc-tube'> = {
name: 'ntc-tube',
label: 'NtC Tube',
category: ColorTheme.Category.Residue,
factory: NtCTubeColorTheme,
getParams: getNtCTubeColorThemeParams,
defaultValues: PD.getDefaultValues(NtCTubeColorThemeParams),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.models.some(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? NtCTubeProvider.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
detach: (data) => data.structure && NtCTubeProvider.ref(data.structure.models[0], false)
}
};

View File

@@ -0,0 +1,44 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { Dnatco, DnatcoParams } from '../property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Model } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const NtCTubeParams = { ...DnatcoParams };
export type NtCTubeParams = typeof NtCTubeParams;
export type NtCTubeProps = PD.Values<NtCTubeParams>;
export type NtCTubeData = PropertyWrapper<NTT.Data | undefined>;
async function fromCif(ctx: CustomProperty.Context, model: Model, props: NtCTubeProps): Promise<CustomProperty.Data<NtCTubeData>> {
const info = PropertyWrapper.createInfo();
const data = Dnatco.getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const steps = Dnatco.getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: { data: steps } } };
}
export const NtCTubeProvider: CustomModelProperty.Provider<NtCTubeParams, NtCTubeData> = CustomModelProperty.createProvider({
label: 'NtC Tube',
descriptor: CustomPropertyDescriptor({
name: 'ntc-tube',
}),
type: 'static',
defaultParams: NtCTubeParams,
getParams: (data: Model) => NtCTubeParams,
isApplicable: (data: Model) => Dnatco.isApplicable(data),
obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<NtCTubeProps>) => {
const p = { ...PD.getDefaultValues(NtCTubeParams), ...props };
return fromCif(ctx, data, p);
}
});

View File

@@ -0,0 +1,454 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeProvider } from './property';
import { NtCTubeSegmentsIterator } from './util';
import { NtCTubeTypes as NTT } from './types';
import { Dnatco } from '../property';
import { DnatcoTypes } from '../types';
import { DnatcoUtil } from '../util';
import { Interval } from '../../../mol-data/int';
import { BaseGeometry, VisualQuality } from '../../../mol-geo/geometry/base';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { addFixedCountDashedCylinder } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { addTube } from '../../../mol-geo/geometry/mesh/builder/tube';
import { PickingId } from '../../../mol-geo/geometry/picking';
import { CylinderProps } from '../../../mol-geo/primitive/cylinder';
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { smoothstep } from '../../../mol-math/interpolate';
import { NullLocation } from '../../../mol-model/location';
import { EmptyLoci, Loci } from '../../../mol-model/loci';
import { Structure, StructureElement, Unit } from '../../../mol-model/structure';
import { structureUnion } from '../../../mol-model/structure/query/utils/structure-set';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
import { createCurveSegmentState, CurveSegmentState } from '../../../mol-repr/structure/visual/util/polymer';
import { getStructureQuality, VisualUpdateState } from '../../../mol-repr/util';
import { VisualContext } from '../../../mol-repr/visual';
import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
const v3add = Vec3.add;
const v3copy = Vec3.copy;
const v3cross = Vec3.cross;
const v3fromArray = Vec3.fromArray;
const v3matchDirection = Vec3.matchDirection;
const v3normalize = Vec3.normalize;
const v3orthogonalize = Vec3.orthogonalize;
const v3scale = Vec3.scale;
const v3slerp = Vec3.slerp;
const v3spline = Vec3.spline;
const v3sub = Vec3.sub;
const v3toArray = Vec3.toArray;
const NtCTubeMeshParams = {
...UnitsMeshParams,
linearSegments: PD.Numeric(4, { min: 2, max: 8, step: 1 }, BaseGeometry.CustomQualityParamInfo),
radialSegments: PD.Numeric(22, { min: 4, max: 56, step: 2 }, BaseGeometry.CustomQualityParamInfo),
residueMarkerWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
segmentBoundaryWidth: PD.Numeric(0.05, { min: 0.01, max: 0.25, step: 0.01 }),
};
type NtCTubeMeshParams = typeof NtCTubeMeshParams;
type QualityOptions = Exclude<VisualQuality, 'auto' | 'custom'>;
const LinearSegmentCount: Record<QualityOptions, number> = {
highest: 6,
higher: 6,
high: 4,
medium: 4,
low: 3,
lower: 3,
lowest: 2,
};
const RadialSegmentCount: Record<QualityOptions, number> = {
highest: 32,
higher: 26,
high: 22,
medium: 18,
low: 14,
lower: 10,
lowest: 6,
};
const _curvePoint = Vec3();
const _tanA = Vec3();
const _tanB = Vec3();
const _firstTangentVec = Vec3();
const _lastTangentVec = Vec3();
const _firstNormalVec = Vec3();
const _lastNormalVec = Vec3();
const _tmpNormal = Vec3();
const _tangentVec = Vec3();
const _normalVec = Vec3();
const _binormalVec = Vec3();
const _prevNormal = Vec3();
const _nextNormal = Vec3();
function interpolatePointsAndTangents(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, tRange: number[]) {
const { curvePoints, tangentVectors, linearSegments } = state;
const tension = 0.5;
const r = tRange[1] - tRange[0];
for (let j = 0; j <= linearSegments; ++j) {
const t = j * r / linearSegments + tRange[0];
v3spline(_curvePoint, p0, p1, p2, p3, t, tension);
v3spline(_tanA, p0, p1, p2, p3, t - 0.01, tension);
v3spline(_tanB, p0, p1, p2, p3, t + 0.01, tension);
v3toArray(_curvePoint, curvePoints, j * 3);
v3normalize(_tangentVec, v3sub(_tangentVec, _tanA, _tanB));
v3toArray(_tangentVec, tangentVectors, j * 3);
}
}
function interpolateNormals(state: CurveSegmentState, firstDirection: Vec3, lastDirection: Vec3) {
const { curvePoints, tangentVectors, normalVectors, binormalVectors } = state;
const n = curvePoints.length / 3;
v3fromArray(_firstTangentVec, tangentVectors, 0);
v3fromArray(_lastTangentVec, tangentVectors, (n - 1) * 3);
v3orthogonalize(_firstNormalVec, _firstTangentVec, firstDirection);
v3orthogonalize(_lastNormalVec, _lastTangentVec, lastDirection);
v3matchDirection(_lastNormalVec, _lastNormalVec, _firstNormalVec);
v3copy(_prevNormal, _firstNormalVec);
const n1 = n - 1;
for (let i = 0; i < n; ++i) {
const j = smoothstep(0, n1, i) * n1;
const t = i === 0 ? 0 : 1 / (n - j);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3orthogonalize(_normalVec, _tangentVec, v3slerp(_tmpNormal, _prevNormal, _lastNormalVec, t));
v3toArray(_normalVec, normalVectors, i * 3);
v3copy(_prevNormal, _normalVec);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
for (let i = 1; i < n1; ++i) {
v3fromArray(_prevNormal, normalVectors, (i - 1) * 3);
v3fromArray(_normalVec, normalVectors, i * 3);
v3fromArray(_nextNormal, normalVectors, (i + 1) * 3);
v3scale(_normalVec, v3add(_normalVec, _prevNormal, v3add(_normalVec, _nextNormal, _normalVec)), 1 / 3);
v3toArray(_normalVec, normalVectors, i * 3);
v3fromArray(_tangentVec, tangentVectors, i * 3);
v3normalize(_binormalVec, v3cross(_binormalVec, _tangentVec, _normalVec));
v3toArray(_binormalVec, binormalVectors, i * 3);
}
}
function interpolate(state: CurveSegmentState, p0: Vec3, p1: Vec3, p2: Vec3, p3: Vec3, firstDir: Vec3, lastDir: Vec3, tRange = [0, 1]) {
interpolatePointsAndTangents(state, p0, p1, p2, p3, tRange);
interpolateNormals(state, firstDir, lastDir);
}
function createNtCTubeSegmentsIterator(structureGroup: StructureGroup): LocationIterator {
const { structure, group } = structureGroup;
const instanceCount = group.units.length;
const data = NtCTubeProvider.get(structure.model)?.value?.data;
if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
const numBlocks = data.data.steps.length * 4;
const getLocation = (groupId: number, instanceId: number) => {
if (groupId > numBlocks) return NullLocation;
const stepIdx = Math.floor(groupId / 4);
const step = data.data.steps[stepIdx];
const r = groupId % 4;
const kind =
r === 0 ? 'upper' :
r === 1 ? 'lower' :
r === 2 ? 'residue-boundary' : 'segment-boundary';
return NTT.Location({ step, kind });
};
return LocationIterator(totalMeshGroupsCount(data.data.steps) + 1, instanceCount, 1, getLocation);
}
function segmentCount(structure: Structure, props: PD.Values<NtCTubeMeshParams>): { linear: number, radial: number } {
const quality = props.quality;
if (quality === 'custom')
return { linear: props.linearSegments, radial: props.radialSegments };
else if (quality === 'auto') {
const autoQuality = getStructureQuality(structure) as QualityOptions;
return { linear: LinearSegmentCount[autoQuality], radial: RadialSegmentCount[autoQuality] };
} else
return { linear: LinearSegmentCount[quality], radial: RadialSegmentCount[quality] };
}
function stepBoundingSphere(step: DnatcoTypes.Step, struLoci: StructureElement.Loci): Sphere3D | undefined {
const one = DnatcoUtil.residueToLoci(step.auth_asym_id_1, step.auth_seq_id_1, step.label_alt_id_1, step.PDB_ins_code_1, struLoci, 'auth');
const two = DnatcoUtil.residueToLoci(step.auth_asym_id_2, step.auth_seq_id_2, step.label_alt_id_2, step.PDB_ins_code_2, struLoci, 'auth');
if (StructureElement.Loci.is(one) && StructureElement.Loci.is(two)) {
const union = structureUnion(struLoci.structure, [StructureElement.Loci.toStructure(one), StructureElement.Loci.toStructure(two)]);
return union.boundary.sphere;
}
return void 0;
}
function totalMeshGroupsCount(steps: DnatcoTypes.Step[]) {
// Each segment has two blocks, Residue Boundary marker and a Segment Boundary marker
return steps.length * 4 - 1; // Subtract one because the last Segment Boundary marker is not drawn
}
function createNtCTubeMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<NtCTubeMeshParams>, mesh?: Mesh) {
if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
const prop = NtCTubeProvider.get(structure.model).value;
if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
const { data } = prop.data;
if (data.steps.length === 0) return Mesh.createEmpty(mesh);
const MarkerLinearSegmentCount = 2;
const segCount = segmentCount(structure, props);
const vertexCount = Math.floor((segCount.linear * 4 * data.steps.length / structure.model.atomicHierarchy.chains._rowCount) * segCount.radial);
const chunkSize = Math.floor(vertexCount / 3);
const diameter = 1.0 * theme.size.props.value;
const mb = MeshBuilder.createState(vertexCount, chunkSize, mesh);
const state = createCurveSegmentState(segCount.linear);
const { curvePoints, normalVectors, binormalVectors, widthValues, heightValues } = state;
for (let idx = 0; idx <= segCount.linear; idx++) {
widthValues[idx] = diameter;
heightValues[idx] = diameter;
}
const [normals, binormals] = [binormalVectors, normalVectors]; // Needed so that the tube is not drawn from inside out
const markerState = createCurveSegmentState(MarkerLinearSegmentCount);
const { curvePoints: mCurvePoints, normalVectors: mNormalVectors, binormalVectors: mBinormalVectors, widthValues: mWidthValues, heightValues: mHeightValues } = markerState;
for (let idx = 0; idx <= MarkerLinearSegmentCount; idx++) {
mWidthValues[idx] = diameter;
mHeightValues[idx] = diameter;
}
const [mNormals, mBinormals] = [mBinormalVectors, mNormalVectors];
const firstDir = Vec3();
const lastDir = Vec3();
const markerDir = Vec3();
const residueMarkerWidth = props.residueMarkerWidth / 2;
const it = new NtCTubeSegmentsIterator(structure, unit);
while (it.hasNext) {
const segment = it.move();
if (!segment)
continue;
const { p_1, p0, p1, p2, p3, p4, pP } = segment;
const FirstBlockId = segment.stepIdx * 4;
const SecondBlockId = FirstBlockId + 1;
const ResidueMarkerId = FirstBlockId + 2;
const SegmentBoundaryMarkerId = FirstBlockId + 3;
const { rmShift, rmPos } = calcResidueMarkerShift(p2, p3, pP);
if (segment.firstInChain) {
v3normalize(firstDir, v3sub(firstDir, p2, p1));
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
} else {
v3copy(firstDir, lastDir);
v3normalize(lastDir, v3sub(lastDir, rmPos, p2));
}
// C5' -> O3' block
interpolate(state, p0, p1, p2, p3, firstDir, lastDir);
mb.currentGroup = FirstBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, segment.firstInChain || segment.followsGap, false, 'rounded');
// O3' -> C5' block
v3copy(firstDir, lastDir);
v3normalize(markerDir, v3sub(markerDir, p3, rmPos));
v3normalize(lastDir, v3sub(lastDir, p4, p3));
// From O3' to the residue marker
interpolate(state, p1, p2, p3, p4, firstDir, markerDir, [0, rmShift - residueMarkerWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Residue marker
interpolate(markerState, p1, p2, p3, p4, markerDir, markerDir, [rmShift - residueMarkerWidth, rmShift + residueMarkerWidth]);
mb.currentGroup = ResidueMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
if (segment.capEnd) {
// From the residue marker to C5' of the end
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, true, 'rounded');
} else {
// From the residue marker to C5' of the step boundary marker
interpolate(state, p1, p2, p3, p4, markerDir, lastDir, [rmShift + residueMarkerWidth, 1 - props.segmentBoundaryWidth]);
mb.currentGroup = SecondBlockId;
addTube(mb, curvePoints, normals, binormals, segCount.linear, segCount.radial, widthValues, heightValues, false, false, 'rounded');
// Step boundary marker
interpolate(markerState, p1, p2, p3, p4, lastDir, lastDir, [1 - props.segmentBoundaryWidth, 1]);
mb.currentGroup = SegmentBoundaryMarkerId;
addTube(mb, mCurvePoints, mNormals, mBinormals, MarkerLinearSegmentCount, segCount.radial, mWidthValues, mHeightValues, false, false, 'rounded');
}
if (segment.followsGap) {
const cylinderProps: CylinderProps = {
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, false, cylinderProps);
}
}
const boundingSphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, 1.05);
const m = MeshBuilder.getMesh(mb);
m.setBoundingSphere(boundingSphere);
return m;
}
const _rmvCO = Vec3();
const _rmvPO = Vec3();
const _rmPos = Vec3();
const _HalfPi = Math.PI / 2;
function calcResidueMarkerShift(pO: Vec3, pC: Vec3, pP: Vec3): { rmShift: number, rmPos: Vec3 } {
v3sub(_rmvCO, pC, pO);
v3sub(_rmvPO, pP, pO);
// Project position of P atom on the O3' -> C5' vector
const beta = Vec3.angle(_rmvPO, _rmvCO);
const alpha = _HalfPi - Math.abs(beta);
const lengthMO = Math.cos(alpha) * Vec3.magnitude(_rmvPO);
const shift = lengthMO / Vec3.magnitude(_rmvCO);
v3scale(_rmvCO, _rmvCO, shift);
v3add(_rmPos, _rmvCO, pO);
return { rmShift: shift, rmPos: _rmPos };
}
function getNtCTubeSegmentLoci(pickingId: PickingId, structureGroup: StructureGroup, id: number) {
const { groupId, objectId, instanceId } = pickingId;
if (objectId !== id) return EmptyLoci;
const { structure } = structureGroup;
const unit = structureGroup.group.units[instanceId];
if (!Unit.isAtomic(unit)) return EmptyLoci;
const data = NtCTubeProvider.get(structure.model)?.value?.data ?? undefined;
if (!data) return EmptyLoci;
const MeshGroupsCount = totalMeshGroupsCount(data.data.steps);
if (groupId > MeshGroupsCount) return EmptyLoci;
const stepIdx = Math.floor(groupId / 4);
const bs = stepBoundingSphere(data.data.steps[stepIdx], Structure.toStructureElementLoci(structure));
/*
* NOTE 1) Each step is drawn with 4 mesh groups. We need to divide/multiply by 4 to convert between steps and mesh groups.
* NOTE 2) Molstar will create a mesh only for the asymmetric unit. When the entire biological assembly
* is displayed, Molstar just copies and transforms the mesh. This means that even though the mesh
* might be displayed multiple times, groupIds of the individual blocks in the mesh will be the same.
* If there are multiple copies of a mesh, Molstar needs to be able to tell which block belongs to which copy of the mesh.
* To do that, Molstar adds an offset to groupIds of the copied meshes. Offset is calculated as follows:
*
* offset = NumberOfBlocks * UnitIndex
*
* "NumberOfBlocks" is the number of valid Location objects got from LocationIterator *or* the greatest groupId set by
* the mesh generator - whichever is smaller.
*
* UnitIndex is the index of the Unit the mesh belongs to, starting from 0. (See "unitMap" in the Structure object).
* We can also get this index from the value "instanceId" of the "pickingId" object.
*
* If this offset is not applied, picking a piece of one of the copied meshes would actually pick that piece in the original mesh.
* This is particularly apparent with highlighting - hovering over items in a copied mesh incorrectly highlights those items in the source mesh.
*
* Molstar can take advantage of the fact that ElementLoci has a reference to the Unit object attached to it. Since we cannot attach ElementLoci
* to a step, we need to calculate the offseted groupId here and pass it as part of the DataLoci.
*/
const offsetGroupId = stepIdx * 4 + (MeshGroupsCount + 1) * instanceId;
return NTT.Loci(data.data.steps, [stepIdx], [offsetGroupId], bs);
}
function eachNtCTubeSegment(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
if (NTT.isLoci(loci)) {
const offsetGroupId = loci.elements[0];
return apply(Interval.ofBounds(offsetGroupId, offsetGroupId + 4));
}
return false;
}
function NtCTubeVisual(materialId: number): UnitsVisual<NtCTubeMeshParams> {
return UnitsMeshVisual<NtCTubeMeshParams>({
defaultProps: PD.getDefaultValues(NtCTubeMeshParams),
createGeometry: createNtCTubeMesh,
createLocationIterator: createNtCTubeSegmentsIterator,
getLoci: getNtCTubeSegmentLoci,
eachLocation: eachNtCTubeSegment,
setUpdateState: (state: VisualUpdateState, newProps: PD.Values<NtCTubeMeshParams>, currentProps: PD.Values<NtCTubeMeshParams>) => {
state.createGeometry = (
newProps.quality !== currentProps.quality ||
newProps.residueMarkerWidth !== currentProps.residueMarkerWidth ||
newProps.segmentBoundaryWidth !== currentProps.segmentBoundaryWidth ||
newProps.doubleSided !== currentProps.doubleSided ||
newProps.alpha !== currentProps.alpha ||
newProps.linearSegments !== currentProps.linearSegments ||
newProps.radialSegments !== currentProps.radialSegments
);
}
}, materialId);
}
const NtCTubeVisuals = {
'ntc-tube-symbol': (ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeMeshParams>) => UnitsRepresentation('NtC Tube Mesh', ctx, getParams, NtCTubeVisual),
};
export const NtCTubeParams = {
...NtCTubeMeshParams
};
export type NtCTubeParams = typeof NtCTubeParams;
export function getNtCTubeParams(ctx: ThemeRegistryContext, structure: Structure) {
return PD.clone(NtCTubeParams);
}
export type NtCTubeRepresentation = StructureRepresentation<NtCTubeParams>;
export function NtCTubeRepresentation(ctx: RepresentationContext, getParams: RepresentationParamsGetter<Structure, NtCTubeParams>): NtCTubeRepresentation {
return Representation.createMulti('NtC Tube', ctx, getParams, StructureRepresentationStateBuilder, NtCTubeVisuals as unknown as Representation.Def<Structure, NtCTubeParams>);
}
export const NtCTubeRepresentationProvider = StructureRepresentationProvider({
name: 'ntc-tube',
label: 'NtC Tube',
description: 'Displays schematic representation of NtC conformers',
factory: NtCTubeRepresentation,
getParams: getNtCTubeParams,
defaultValues: PD.getDefaultValues(NtCTubeParams),
defaultColorTheme: { name: 'ntc-tube' },
defaultSizeTheme: { name: 'uniform', props: { value: 2.0 } },
isApplicable: (structure: Structure) => structure.models.every(m => Dnatco.isApplicable(m)),
ensureCustomProperties: {
attach: async (ctx: CustomProperty.Context, structure: Structure) => structure.models.forEach(m => NtCTubeProvider.attach(ctx, m, void 0, true)),
detach: (data) => data.models.forEach(m => NtCTubeProvider.ref(m, false)),
},
});

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeSegmentLabel } from './behavior';
import { DnatcoTypes } from '../types';
import { Sphere3D } from '../../../mol-math/geometry/primitives/sphere3d';
import { DataLocation } from '../../../mol-model/location';
import { DataLoci } from '../../../mol-model/loci';
export namespace NtCTubeTypes {
const DataTag = 'dnatco-tube-segment-data';
const DummyTag = 'dnatco-tube-dummy';
export type Data = {
data: DnatcoTypes.Steps,
}
export type TubeBlock = {
step: DnatcoTypes.Step,
kind: 'upper' | 'lower' | 'residue-boundary' | 'segment-boundary';
}
export interface Location extends DataLocation<TubeBlock> {}
export function Location(payload: TubeBlock) {
return DataLocation(DataTag, payload, {});
}
export function isLocation(x: any): x is Location {
return !!x && x.kind === 'data-location' && x.tag === DataTag;
}
export interface Loci extends DataLoci<DnatcoTypes.Step[], number> {}
export interface DummyLoci extends DataLoci<{}, number> {}
export function Loci(data: DnatcoTypes.Step[], stepIndices: number[], elements: number[], boundingSphere?: Sphere3D): Loci {
return DataLoci(DataTag, data, elements, boundingSphere ? () => boundingSphere : undefined, () => stepIndices[0] !== undefined ? NtCTubeSegmentLabel(data[stepIndices[0]]) : '');
}
export function DummyLoci(): DummyLoci {
return DataLoci(DummyTag, {}, [], undefined, () => '');
}
export function isLoci(x: any): x is Loci {
return !!x && x.kind === 'data-loci' && x.tag === DataTag;
}
}

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { NtCTubeTypes as NTT } from './types';
import { NtCTubeProvider } from './property';
import { DnatcoUtil } from '../util';
import { Segmentation, SortedArray } from '../../../mol-data/int';
import { Vec3 } from '../../../mol-math/linear-algebra';
import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
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) {
loc.unit.conformation.invariantPosition(eI, vec);
return true;
}
return false; // Atom not found
}
const p_1 = Vec3();
const p0 = Vec3();
const p1 = Vec3();
const p2 = Vec3();
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) {
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 };
}
function hasGapElements(r: DnatcoUtil.Residue, unit: Unit) {
for (let xI = r.start; xI < r.end; xI++) {
const eI = unit.elements[xI];
if (SortedArray.has(unit.gapElements, eI)) {
return true;
}
}
return false;
}
export type NtCTubeSegment = {
p_1: Vec3,
p0: Vec3,
p1: Vec3,
p2: Vec3,
p3: Vec3,
p4: Vec3,
pP: Vec3,
stepIdx: number,
followsGap: boolean,
firstInChain: boolean,
capEnd: boolean,
}
export class NtCTubeSegmentsIterator {
private chainIt: Segmentation.SegmentIterator<ChainIndex>;
private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
/* Second residue of the previous step, may be undefined
* if we are at the beginning of a chain or right after a discontinuity */
private residuePrev?: DnatcoUtil.Residue;
/* First residue of the current step */
private residueOne?: DnatcoUtil.Residue;
/* Second residue of the current step */
private residueTwo: DnatcoUtil.Residue;
/* First residue of the next step, may be undefined
* if we are at the end of a chain.
* Undefined value indicates that the iterator has reached the end.*/
private residueNext?: DnatcoUtil.Residue;
private data?: NTT.Data;
private altIdOne = '';
private insCodeOne = '';
private loc: StructureElement.Location;
private moveStep() {
if (!this.residueNext)
return void 0;
/* Assume discontinuity of the ResidueIndex of the residue that would become residue one (= first residue of the corresponding step)
* does not equal to ResidueIndex of what would be residue two (= second residue of the corresponding step). */
if (this.residueTwo.index + 1 === this.residueNext.index) {
this.residuePrev = DnatcoUtil.copyResidue(this.residueOne);
this.residueOne = DnatcoUtil.copyResidue(this.residueTwo);
this.residueTwo = DnatcoUtil.copyResidue(this.residueNext)!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
} else {
if (!this.residueIt.hasNext) {
this.residueNext = void 0;
return void 0;
}
// There is discontinuity, act as if we were at the beginning of a chain
this.residuePrev = void 0;
this.residueOne = DnatcoUtil.copyResidue(this.residueNext);
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
this.residueNext = this.residueIt.hasNext ? DnatcoUtil.copyResidue(this.residueIt.move())! : void 0;
}
return this.toSegment(this.residuePrev, this.residueOne!, this.residueTwo, this.residueNext);
}
private prime() {
if (this.residueIt.hasNext)
this.residueTwo = DnatcoUtil.copyResidue(this.residueIt.move())!;
if (this.residueIt.hasNext)
this.residueNext = this.residueIt.move();
}
private toSegment(r0: DnatcoUtil.Residue | undefined, r1: DnatcoUtil.Residue, r2: DnatcoUtil.Residue, r3: DnatcoUtil.Residue | undefined): NtCTubeSegment | undefined {
const indices = DnatcoUtil.getStepIndices(this.data!.data, this.loc, r1);
if (indices.length === 0)
return void 0;
const stepIdx = indices[0];
const step = this.data!.data.steps[stepIdx];
const altIdPrev = this.altIdOne;
const insCodePrev = this.insCodeOne;
this.altIdOne = step.label_alt_id_1;
this.insCodeOne = step.PDB_ins_code_1;
const altIdTwo = step.label_alt_id_2;
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 {
...points,
stepIdx,
followsGap,
firstInChain: !r0,
capEnd: !this.residueNext || precedesDiscontinuity || hasGapElements(r2, this.loc.unit),
};
}
constructor(structure: Structure, unit: Unit.Atomic) {
this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
const prop = NtCTubeProvider.get(unit.model).value;
this.data = prop?.data;
if (this.chainIt.hasNext) {
this.residueIt.setSegment(this.chainIt.move());
this.prime();
}
this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
}
get hasNext() {
if (!this.data)
return false;
return !!this.residueNext
? true
: this.chainIt.hasNext;
}
move() {
if (!!this.residueNext) {
return this.moveStep();
} else {
this.residuePrev = void 0; // Assume discontinuity when we switch chains
this.residueNext = void 0;
this.residueIt.setSegment(this.chainIt.move());
this.prime();
return this.moveStep();
}
}
}

View File

@@ -0,0 +1,172 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { Column, Table } from '../../mol-data/db';
import { toTable } from '../../mol-io/reader/cif/schema';
import { Model } from '../../mol-model/structure';
import { CustomProperty } from '../../mol-model-props/common/custom-property';
import { PropertyWrapper } from '../../mol-model-props/common/wrapper';
import { MmcifFormat } from '../../mol-model-formats/structure/mmcif';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
export type DnatcoSteps = PropertyWrapper<DnatcoTypes.Steps | undefined>;
export const DnatcoParams = {};
export type DnatcoParams = typeof DnatcoParams;
export type DnatcoProps = PD.Values<DnatcoParams>;
export namespace Dnatco {
export const Schema = {
ndb_struct_ntc_step: {
id: Column.Schema.int,
name: Column.Schema.str,
PDB_model_number: Column.Schema.int,
label_entity_id_1: Column.Schema.int,
label_asym_id_1: Column.Schema.str,
label_seq_id_1: Column.Schema.int,
label_comp_id_1: Column.Schema.str,
label_alt_id_1: Column.Schema.str,
label_entity_id_2: Column.Schema.int,
label_asym_id_2: Column.Schema.str,
label_seq_id_2: Column.Schema.int,
label_comp_id_2: Column.Schema.str,
label_alt_id_2: Column.Schema.str,
auth_asym_id_1: Column.Schema.str,
auth_seq_id_1: Column.Schema.int,
auth_asym_id_2: Column.Schema.str,
auth_seq_id_2: Column.Schema.int,
PDB_ins_code_1: Column.Schema.str,
PDB_ins_code_2: Column.Schema.str,
},
ndb_struct_ntc_step_summary: {
step_id: Column.Schema.int,
assigned_CANA: Column.Schema.str,
assigned_NtC: Column.Schema.str,
confal_score: Column.Schema.int,
euclidean_distance_NtC_ideal: Column.Schema.float,
cartesian_rmsd_closest_NtC_representative: Column.Schema.float,
closest_CANA: Column.Schema.str,
closest_NtC: Column.Schema.str,
closest_step_golden: Column.Schema.str
}
};
export type Schema = typeof Schema;
export function getStepsFromCif(
model: Model,
cifSteps: Table<typeof Dnatco.Schema.ndb_struct_ntc_step>,
stepsSummary: StepsSummaryTable
): DnatcoTypes.Steps {
const steps = new Array<DnatcoTypes.Step>();
const mapping = new Array<DnatcoTypes.MappedChains>();
const {
id, PDB_model_number, name,
auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
_rowCount
} = cifSteps;
if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
for (let i = 0; i < _rowCount; i++) {
const {
NtC,
confal_score,
rmsd
} = getSummaryData(id.value(i), i, stepsSummary);
const modelNum = PDB_model_number.value(i);
const chainId = auth_asym_id_1.value(i);
const seqId = auth_seq_id_1.value(i);
const modelIdx = modelNum - 1;
if (mapping.length <= modelIdx || !mapping[modelIdx])
mapping[modelIdx] = new Map<string, DnatcoTypes.MappedResidues>();
const step = {
PDB_model_number: modelNum,
name: name.value(i),
auth_asym_id_1: chainId,
auth_seq_id_1: seqId,
label_comp_id_1: label_comp_id_1.value(i),
label_alt_id_1: label_alt_id_1.value(i),
PDB_ins_code_1: PDB_ins_code_1.value(i),
auth_asym_id_2: auth_asym_id_2.value(i),
auth_seq_id_2: auth_seq_id_2.value(i),
label_comp_id_2: label_comp_id_2.value(i),
label_alt_id_2: label_alt_id_2.value(i),
PDB_ins_code_2: PDB_ins_code_2.value(i),
confal_score,
NtC,
rmsd,
};
steps.push(step);
const mappedChains = mapping[modelIdx];
const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
const stepsForResidue = residuesOnChain.get(seqId) ?? [];
stepsForResidue.push(steps.length - 1);
residuesOnChain.set(seqId, stepsForResidue);
mappedChains.set(chainId, residuesOnChain);
mapping[modelIdx] = mappedChains;
}
return { steps, mapping };
}
export async function fromCif(ctx: CustomProperty.Context, model: Model, props: DnatcoProps): Promise<CustomProperty.Data<DnatcoSteps>> {
const info = PropertyWrapper.createInfo();
const data = getCifData(model);
if (data === undefined) return { value: { info, data: undefined } };
const fromCif = getStepsFromCif(model, data.steps, data.stepsSummary);
return { value: { info, data: fromCif } };
}
export function getCifData(model: Model) {
if (!MmcifFormat.is(model.sourceData)) throw new Error('Data format must be mmCIF');
if (!hasNdbStructNtcCategories(model)) return undefined;
return {
steps: toTable(Schema.ndb_struct_ntc_step, model.sourceData.data.frame.categories.ndb_struct_ntc_step),
stepsSummary: toTable(Schema.ndb_struct_ntc_step_summary, model.sourceData.data.frame.categories.ndb_struct_ntc_step_summary)
};
}
function hasNdbStructNtcCategories(model: Model): boolean {
if (!MmcifFormat.is(model.sourceData)) return false;
const names = (model.sourceData).data.frame.categoryNames;
return names.includes('ndb_struct_ntc_step') && names.includes('ndb_struct_ntc_step_summary');
}
export function isApplicable(model?: Model): boolean {
return !!model && hasNdbStructNtcCategories(model);
}
}
type StepsSummaryTable = Table<typeof Dnatco.Schema.ndb_struct_ntc_step_summary>;
function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
const {
step_id,
confal_score,
assigned_NtC,
cartesian_rmsd_closest_NtC_representative,
} = stepsSummary;
// Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
for (let j = i; j < stepsSummary._rowCount; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
// Safety net for cases where the previous assumption is not met
for (let j = 0; j < i; j++) {
if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
}
throw new Error('Inconsistent mmCIF data');
}

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
export namespace DnatcoTypes {
export const DataTag = 'dnatco-confal-half-step';
export type Step = {
PDB_model_number: number,
name: string,
auth_asym_id_1: string,
auth_seq_id_1: number,
label_comp_id_1: string,
label_alt_id_1: string,
PDB_ins_code_1: string,
auth_asym_id_2: string,
auth_seq_id_2: number,
label_comp_id_2: string,
label_alt_id_2: string,
PDB_ins_code_2: string,
confal_score: number,
NtC: string,
rmsd: number,
}
export type MappedChains = Map<string, MappedResidues>;
export type MappedResidues = Map<number, number[]>;
export interface Steps {
steps: Array<Step>,
mapping: MappedChains[],
}
export interface HalfStep {
step: Step,
isLower: boolean,
}
}

View File

@@ -0,0 +1,117 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Michal Malý <michal.maly@ibt.cas.cz>
* @author Jiří Černý <jiri.cerny@ibt.cas.cz>
*/
import { DnatcoTypes } from './types';
import { OrderedSet, Segmentation } from '../../mol-data/int';
import { EmptyLoci } from '../../mol-model/loci';
import { ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../mol-model/structure';
const EmptyStepIndices = new Array<number>();
export namespace DnatcoUtil {
export type Residue = Segmentation.Segment<ResidueIndex>;
export function copyResidue(r?: Residue) {
return r ? { index: r.index, start: r.start, end: r.end } : void 0;
}
export function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string, insCode: string): ElementIndex {
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = loc.unit.elements[eI];
const elName = StructureProperties.atom.label_atom_id(loc);
const elAltId = StructureProperties.atom.label_alt_id(loc);
const elInsCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
if (names.includes(elName) && (elAltId === altId || elAltId.length === 0) && (elInsCode === insCode))
return loc.element;
}
return -1 as ElementIndex;
}
export function getStepIndices(data: DnatcoTypes.Steps, loc: StructureElement.Location, r: DnatcoUtil.Residue) {
loc.element = loc.unit.elements[r.start];
const modelIdx = StructureProperties.unit.model_num(loc) - 1;
const chainId = StructureProperties.chain.auth_asym_id(loc);
const seqId = StructureProperties.residue.auth_seq_id(loc);
const insCode = StructureProperties.residue.pdbx_PDB_ins_code(loc);
const chains = data.mapping[modelIdx];
if (!chains) return EmptyStepIndices;
const residues = chains.get(chainId);
if (!residues) return EmptyStepIndices;
const indices = residues.get(seqId);
if (!indices) return EmptyStepIndices;
return insCode !== '' ? indices.filter(idx => data.steps[idx].PDB_ins_code_1 === insCode) : indices;
}
export function residueAltIds(structure: Structure, unit: Unit, residue: Residue) {
const altIds = new Array<string>();
const loc = StructureElement.Location.create(structure, unit);
for (let eI = residue.start; eI < residue.end; eI++) {
loc.element = OrderedSet.getAt(unit.elements, eI);
const altId = StructureProperties.atom.label_alt_id(loc);
if (altId !== '' && !altIds.includes(altId))
altIds.push(altId);
}
return altIds;
}
const _loc = StructureElement.Location.create();
export function residueToLoci(asymId: string, seqId: number, altId: string | undefined, insCode: string, loci: StructureElement.Loci, source: 'label' | 'auth') {
_loc.structure = loci.structure;
for (const e of loci.elements) {
_loc.unit = e.unit;
const getAsymId = source === 'label' ? StructureProperties.chain.label_asym_id : StructureProperties.chain.auth_asym_id;
const getSeqId = source === 'label' ? StructureProperties.residue.label_seq_id : StructureProperties.residue.auth_seq_id;
// Walk the entire unit and look for the requested residue
const chainIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.chainAtomSegments, e.unit.elements);
const residueIt = Segmentation.transientSegments(e.unit.model.atomicHierarchy.residueAtomSegments, e.unit.elements);
const elemIndex = (idx: number) => OrderedSet.getAt(e.unit.elements, idx);
while (chainIt.hasNext) {
const chain = chainIt.move();
_loc.element = elemIndex(chain.start);
const _asymId = getAsymId(_loc);
if (_asymId !== asymId)
continue; // Wrong chain, skip it
residueIt.setSegment(chain);
while (residueIt.hasNext) {
const residue = residueIt.move();
_loc.element = elemIndex(residue.start);
const _seqId = getSeqId(_loc);
if (_seqId === seqId) {
const _insCode = StructureProperties.residue.pdbx_PDB_ins_code(_loc);
if (_insCode !== insCode)
continue;
if (altId) {
const _altIds = residueAltIds(loci.structure, e.unit, residue);
if (!_altIds.includes(altId))
continue;
}
const start = residue.start as StructureElement.UnitIndex;
const end = residue.end as StructureElement.UnitIndex;
return StructureElement.Loci(
loci.structure,
[{ unit: e.unit, indices: OrderedSet.ofBounds(start, end) }]
);
}
}
}
}
return EmptyLoci;
}
}

View File

@@ -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),
}]
});
}

View File

@@ -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,15 +370,96 @@ 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) {
@@ -371,9 +473,24 @@ export abstract class MeshExporter<D extends RenderObjectExportData> implements
const sphereCount = vertexCount / 4 * 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);
@@ -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>;

View File

@@ -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);

View File

@@ -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();

View File

@@ -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) {

View File

@@ -0,0 +1,187 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Testing examples for using mesh-extension.ts. */
import { CIF } from '../../mol-io/reader/cif';
import { Volume } from '../../mol-model/volume';
import { createStructureRepresentationParams } from '../../mol-plugin-state/helpers/structure-representation-params';
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 { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
import { Asset } from '../../mol-util/assets';
import { Color } from '../../mol-util/color';
import { ParamDefinition } from '../../mol-util/param-definition';
import { createMeshFromUrl } from './mesh-extension';
import { MeshServerInfo } from './mesh-streaming/server-info';
import { InitMeshStreaming } from './mesh-streaming/transformers';
import * as MeshUtils from './mesh-utils';
export const DB_URL = '/db'; // local
export async function runMeshExtensionExamples(plugin: PluginContext, db_url: string = DB_URL) {
console.time('TIME MESH EXAMPLES');
// await runIsosurfaceExample(plugin, db_url);
// await runMolsurfaceExample(plugin);
// Focused Ion Beam-Scanning Electron Microscopy of mitochondrial reticulum in murine skeletal muscle: https://www.ebi.ac.uk/empiar/EMPIAR-10070/
// await runMeshExample(plugin, 'all', db_url);
// await runMeshExample(plugin, 'fg', db_url);
// await runMultimeshExample(plugin, 'fg', 'worst', db_url);
// await runCifMeshExample(plugin);
// await runMeshExample2(plugin, 'fg');
await runMeshStreamingExample(plugin);
console.timeEnd('TIME MESH EXAMPLES');
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample(plugin: PluginContext, segments: 'fg' | 'all', db_url: string = DB_URL) {
const detail = 2;
const segmentIds = (segments === 'all') ?
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-mesh-rounded/segment-${segmentId}/detail-${detail}`, segmentId, detail, true, undefined);
}
}
/** Example for downloading multiple separate segments, each containing 1 mesh. */
export async function runMeshExample2(plugin: PluginContext, segments: 'one' | 'few' | 'fg' | 'all') {
const detail = 1;
const segmentIds = (segments === 'one') ? [15]
: (segments === 'few') ? [1, 4, 7, 10, 16]
: (segments === 'all') ? [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17] // segment-16 has no detail-2
: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 17]; // segment-13 and segment-15 are quasi background
for (const segmentId of segmentIds) {
await createMeshFromUrl(plugin, `http://localhost:9000/v2/empiar/empiar-10070/mesh_bcif/${segmentId}/${detail}`, segmentId, detail, false, undefined);
}
}
/** Example for downloading a single segment containing multiple meshes. */
export async function runMultimeshExample(plugin: PluginContext, segments: 'fg' | 'all', detailChoice: 'best' | 'worst', db_url: string = DB_URL) {
const urlDetail = (detailChoice === 'best') ? '2' : 'worst';
const numDetail = (detailChoice === 'best') ? 2 : 1000;
await createMeshFromUrl(plugin, `${db_url}/empiar-10070-multimesh-rounded/segments-${segments}/detail-${urlDetail}`, 0, numDetail, false, undefined);
}
export async function runMeshStreamingExample(plugin: PluginContext, source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070', serverUrl?: string, parent?: StateObjectSelector) {
const params = ParamDefinition.getDefaultValues(MeshServerInfo.Params);
if (serverUrl) params.serverUrl = serverUrl;
params.source = source;
params.entryId = entryId;
await plugin.runTask(plugin.state.data.applyAction(InitMeshStreaming, params, parent?.ref), { useOverlay: false });
}
/** Example for downloading a protein structure and visualizing molecular surface. */
export async function runMolsurfaceExample(plugin: PluginContext) {
const entryId = 'pdb-7etq';
// Node "https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif" ("transformer": "ms-plugin.download") -> var data
const data = await plugin.builders.data.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/7etq.bcif', isBinary: true }, { state: { isGhost: false } });
console.log('formats:', plugin.dataFormats.list);
// Node "CIF File" ("transformer": "ms-plugin.parse-cif")
// Node "7ETQ 1 model" ("transformer": "ms-plugin.trajectory-from-mmcif") -> var trajectory
const parsed = await plugin.dataFormats.get('mmcif')!.parse(plugin, data, { entryId });
const trajectory: StateObjectSelector<PluginStateObject.Molecule.Trajectory> = parsed.trajectory;
console.log('parsed', parsed);
console.log('trajectory', trajectory);
// Node "Model 1" ("transformer": "ms-plugin.model-from-trajectory") -> var model
const model = await plugin.build().to(trajectory).apply(StateTransforms.Model.ModelFromTrajectory).commit();
console.log('model:', model);
// Node "Model 91 elements" ("transformer": "ms-plugin.structure-from-model") -> var structure
const structure = await plugin.build().to(model).apply(StateTransforms.Model.StructureFromModel,).commit();
console.log('structure:', structure);
// Node "Molecular Surface" ("transformer": "ms-plugin.structure-representation-3d") -> var repr
const reprParams = createStructureRepresentationParams(plugin, undefined, { type: 'molecular-surface' });
const repr = await plugin.build().to(structure).apply(StateTransforms.Representation.StructureRepresentation3D, reprParams).commit();
console.log('repr:', repr);
}
/** Example for downloading an EMDB density data and visualizing isosurface. */
export async function runIsosurfaceExample(plugin: PluginContext, db_url: string = DB_URL) {
const entryId = 'emd-1832';
const isoLevel = 2.73;
let root = await plugin.build();
const data = await plugin.builders.data.download({ url: `${db_url}/emd-1832-box`, isBinary: true }, { state: { isGhost: false } });
const parsed = await plugin.dataFormats.get('dscif')!.parse(plugin, data, { entryId });
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
const volumeData = volume.cell!.obj!.data;
console.log('data:', data);
console.log('parsed:', parsed);
console.log('volume:', volume);
console.log('volumeData:', volumeData);
root = await plugin.build();
console.log('root:', root);
console.log('to:', root.to(volume));
console.log('toRoot:', root.toRoot());
let volumeParams;
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 0.5,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['solid'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x00aaaa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
volumeParams = createVolumeRepresentationParams(plugin, volumeData, {
type: 'isosurface',
typeParams: {
alpha: 1.0,
isoValue: Volume.adjustedIsoValue(volumeData, isoLevel, 'relative'),
visuals: ['wireframe'],
sizeFactor: 1,
},
color: 'uniform',
colorParams: { value: Color(0x8800aa) },
});
root.to(volume).apply(StateTransforms.Representation.VolumeRepresentation3D, volumeParams);
await root.commit();
}
export async function runCifMeshExample(plugin: PluginContext, api: string = 'http://localhost:9000/v2',
source: MeshServerInfo.MeshSource = 'empiar', entryId: string = 'empiar-10070',
segmentId: number = 1, detail: number = 10,
) {
const url = `${api}/${source}/${entryId}/mesh_bcif/${segmentId}/${detail}`;
getMeshFromBcif(plugin, url);
}
async function getMeshFromBcif(plugin: PluginContext, url: string) {
const urlAsset = Asset.getUrlAsset(plugin.managers.asset, url);
const asset = await plugin.runTask(plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
plugin.log.error('VolumeStreaming, parsing CIF: ' + parsed.toString());
return;
}
console.log('blocks:', parsed.result.blocks);
const mesh = await MeshUtils.meshFromCif(parsed.result);
console.log(mesh);
}

View File

@@ -0,0 +1,40 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Column, Database } from '../../mol-data/db';
import { CifFrame } from '../../mol-io/reader/cif';
import { toDatabase } from '../../mol-io/reader/cif/schema';
const int = Column.Schema.int;
const float = Column.Schema.float;
// TODO in future, move to molstar/src/mol-io/reader/cif/schema/mesh.ts
export const Mesh_Data_Schema = {
mesh: {
id: int,
},
mesh_vertex: {
mesh_id: int,
vertex_id: int,
x: float,
y: float,
z: float,
},
/** Table of triangles, 3 rows per triangle */
mesh_triangle: {
mesh_id: int,
/** Indices of vertices within mesh */
vertex_id: int,
}
};
export type Mesh_Data_Schema = typeof Mesh_Data_Schema;
export interface Mesh_Data_Database extends Database<Mesh_Data_Schema> {}
// TODO in future, move to molstar/src/mol-io/reader/cif.ts: CIF.schema.mesh
export const CIF_schema_mesh = (frame: CifFrame) => toDatabase<Mesh_Data_Schema, Mesh_Data_Database>(Mesh_Data_Schema, frame);

View File

@@ -0,0 +1,223 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Defines new types of State tree transformers for dealing with mesh data. */
import { BaseGeometry, VisualQuality, VisualQualityOptions } from '../../mol-geo/geometry/base';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { CifFile } from '../../mol-io/reader/cif';
import { Box3D } from '../../mol-math/geometry';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Shape } from '../../mol-model/shape';
import { ShapeProvider } from '../../mol-model/shape/provider';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download } from '../../mol-plugin-state/transforms/data';
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectRef, StateObjectSelector, StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import * as MeshUtils from './mesh-utils';
export const BACKGROUND_OPACITY = 0.2;
export const FOREROUND_OPACITY = 1;
export const VolsegTransform: StateTransformer.Builder.Root = StateTransformer.builderFactory('volseg');
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data
/** Data type for `MeshlistStateObject` - list of meshes */
export interface MeshlistData {
segmentId: number,
segmentName: string,
detail: number,
meshIds: number[],
mesh: Mesh,
/** Reference to the object which created this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId?: string,
}
export namespace MeshlistData {
export function empty(): MeshlistData {
return {
segmentId: 0,
segmentName: 'Empty',
detail: 0,
meshIds: [],
mesh: Mesh.createEmpty(),
};
};
export async function fromCIF(data: CifFile, segmentId: number, segmentName: string, detail: number): Promise<MeshlistData> {
const { mesh, meshIds } = await MeshUtils.meshFromCif(data);
return {
segmentId,
segmentName,
detail,
meshIds,
mesh,
};
}
export function stats(meshListData: MeshlistData): string {
return `Meshlist "${meshListData.segmentName}" (detail ${meshListData.detail}): ${meshListData.meshIds.length} meshes, ${meshListData.mesh.vertexCount} vertices, ${meshListData.mesh.triangleCount} triangles`;
}
export function getShape(data: MeshlistData, color: Color): Shape<Mesh> {
const mesh = data.mesh;
const meshShape: Shape<Mesh> = Shape.create(data.segmentName, data, mesh,
() => color,
() => 1,
// group => `${data.segmentName} | Segment ${data.segmentId} | Detail ${data.detail} | Mesh ${group}`,
group => data.segmentName,
);
return meshShape;
}
export function combineBBoxes(boxes: (Box3D | null)[]): Box3D | null {
let result = null;
for (const box of boxes) {
if (!box) continue;
if (result) {
Vec3.min(result.min, result.min, box.min);
Vec3.max(result.max, result.max, box.max);
} else {
result = Box3D.zero();
Box3D.copy(result, box);
}
}
return result;
}
export function bbox(data: MeshlistData): Box3D | null {
return MeshUtils.bbox(data.mesh);
}
export function allVerticesUsed(data: MeshlistData): boolean {
const unusedVertices = new Set();
for (let i = 0; i < data.mesh.vertexCount; i++) {
unusedVertices.add(i);
}
for (let i = 0; i < 3 * data.mesh.triangleCount; i++) {
const v = data.mesh.vertexBuffer.ref.value[i];
unusedVertices.delete(v);
}
return unusedVertices.size === 0;
}
}
// // // // // // // // // // // // // // // // // // // // // // // //
// Raw Data -> Parsed data
export class MeshlistStateObject extends PluginStateObject.Create<MeshlistData>({ name: 'Parsed Meshlist', typeClass: 'Object' }) { }
export const ParseMeshlistTransformer = VolsegTransform({
name: 'meshlist-from-string',
from: PluginStateObject.Format.Cif,
to: MeshlistStateObject,
params: {
label: PD.Text(MeshlistStateObject.type.name, { isHidden: true }),
segmentId: PD.Numeric(1, {}, { isHidden: true }),
segmentName: PD.Text('Segment'),
detail: PD.Numeric(1, {}, { isHidden: true }),
/** Reference to the object which manages this meshlist (e.g. `MeshStreaming.Behavior`) */
ownerId: PD.Text('', { isHidden: true }),
}
})({
apply({ a, params }, globalCtx) { // `a` is the parent node, params are 2nd argument to To.apply(), `globalCtx` is the plugin
return Task.create('Create Parsed Meshlist', async ctx => {
const meshlistData = await MeshlistData.fromCIF(a.data, params.segmentId, params.segmentName, params.detail);
meshlistData.ownerId = params.ownerId;
const es = meshlistData.meshIds.length === 1 ? '' : 'es';
return new MeshlistStateObject(meshlistData, { label: params.label, description: `${meshlistData.segmentName} (${meshlistData.meshIds.length} mesh${es})` });
});
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
// Parsed data -> Shape
/** Data type for PluginStateObject.Shape.Provider */
type MeshShapeProvider = ShapeProvider<MeshlistData, Mesh, Mesh.Params>;
namespace MeshShapeProvider {
export function fromMeshlistData(meshlist: MeshlistData, color?: Color): MeshShapeProvider {
const theColor = color ?? MeshUtils.ColorGenerator.next().value;
return {
label: 'Mesh',
data: meshlist,
params: meshShapeProviderParams,
geometryUtils: Mesh.Utils,
getShape: (ctx, data: MeshlistData) => MeshlistData.getShape(data, theColor),
};
}
}
const meshShapeProviderParams: Mesh.Params = {
...Mesh.Params,
quality: PD.Select<VisualQuality>('custom', VisualQualityOptions, { isEssential: true, description: 'Visual/rendering quality of the representation.' }), // use 'custom' when wanting to apply doubleSided
doubleSided: PD.Boolean(true, BaseGeometry.CustomQualityParamInfo),
// set `flatShaded`: true to see the real mesh vertices and triangles
transparentBackfaces: PD.Select('on', PD.arrayToOptions(['off', 'on', 'opaque']), BaseGeometry.ShadingCategory), // 'on' means: show backfaces with correct opacity, even when opacity < 1 (requires doubleSided) ¯\_(ツ)_/¯
};
export const MeshShapeTransformer = VolsegTransform({
name: 'shape-from-meshlist',
display: { name: 'Shape from Meshlist', description: 'Create Shape from Meshlist data' },
from: MeshlistStateObject,
to: PluginStateObject.Shape.Provider,
params: {
color: PD.Value<Color | undefined>(undefined), // undefined means random color
},
})({
apply({ a, params }) {
const shapeProvider = MeshShapeProvider.fromMeshlistData(a.data, params.color);
return new PluginStateObject.Shape.Provider(shapeProvider, { label: PluginStateObject.Shape.Provider.type.name, description: a.description });
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
/** Download data and create state tree hierarchy down to visual representation. */
export async function createMeshFromUrl(plugin: PluginContext, meshDataUrl: string, segmentId: number, detail: number,
collapseTree: boolean, color?: Color, parent?: StateObjectSelector | StateObjectRef, transparentIfBboxAbove?: number,
name?: string, ownerId?: string) {
const update = parent ? plugin.build().to(parent) : plugin.build().toRoot();
const rawDataNodeRef = update.apply(Download,
{ url: meshDataUrl, isBinary: true, label: `Downloaded Data ${segmentId}` },
{ state: { isCollapsed: collapseTree } }
).ref;
const parsedDataNode = await update.to(rawDataNodeRef)
.apply(StateTransforms.Data.ParseCif)
.apply(ParseMeshlistTransformer,
{ label: undefined, segmentId: segmentId, segmentName: name ?? `Segment ${segmentId}`, detail: detail, ownerId: ownerId },
{}
)
.commit();
let transparent = false;
if (transparentIfBboxAbove !== undefined && parsedDataNode.data) {
const bbox = MeshlistData.bbox(parsedDataNode.data) || Box3D.zero();
transparent = Box3D.volume(bbox) > transparentIfBboxAbove;
}
await plugin.build().to(parsedDataNode)
.apply(MeshShapeTransformer, { color: color },)
.apply(ShapeRepresentation3D,
{ alpha: transparent ? BACKGROUND_OPACITY : FOREROUND_OPACITY },
{ tags: ['mesh-segment-visual', `segment-${segmentId}`] }
)
.commit();
return rawDataNodeRef;
}

View File

@@ -0,0 +1,332 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { distinctUntilChanged, map } from 'rxjs';
import { CIF } from '../../../mol-io/reader/cif';
import { Box3D } from '../../../mol-math/geometry';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { PluginCommand } from '../../../mol-plugin/command';
import { PluginCommands } from '../../../mol-plugin/commands';
import { PluginContext } from '../../../mol-plugin/context';
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 { 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';
import { MeshServerInfo } from './server-info';
const DEFAULT_SEGMENT_NAME = 'Untitled segment';
const DEFAULT_SEGMENT_COLOR = ColorNames.lightgray;
export const NO_SEGMENT = -1;
/** Maximum (worst) detail level available in GUI (TODO set actual maximum possible value) */
const MAX_DETAIL = 10;
const DEFAULT_DETAIL = 7; // TODO decide a reasonable default
/** Segments whose bounding box volume is above this value (relative to the overall bounding box) are considered as background segments */
export const BACKGROUND_SEGMENT_VOLUME_THRESHOLD = 0.5;
export class MeshStreaming extends PluginStateObject.CreateBehavior<MeshStreaming.Behavior>({ name: 'Mesh Streaming' }) { }
export namespace MeshStreaming {
export namespace Params {
export const ViewTypeChoice = new Choice({ off: 'Off', select: 'Select', all: 'All' }, 'select'); // TODO add camera target?
export type ViewType = Choice.Values<typeof ViewTypeChoice>;
export function create(options: MeshServerInfo.Data) {
return {
view: PD.MappedStatic('select', {
'off': PD.Group({}),
'select': PD.Group({
baseDetail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the non-selected segments (lower number = better)' }),
focusDetail: PD.Numeric(1, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for the selected segment (lower number = better)' }),
selectedSegment: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
}, { isFlat: true }),
'all': PD.Group({
detail: PD.Numeric(DEFAULT_DETAIL, { min: 1, max: MAX_DETAIL, step: 1 }, { description: 'Detail level for all segments (lower number = better)' })
}, { isFlat: true }),
}, { description: '"Off" hides all segments. \n"Select" shows all segments in lower detail, clicked segment in better detail. "All" shows all segment in the same level.' }),
};
}
export type Definition = ReturnType<typeof create>
export type Values = PD.Values<Definition>
export function copyValues(params: Values): Values {
return {
view: {
name: params.view.name,
params: { ...params.view.params } as any,
}
};
}
export function valuesEqual(p: Values, q: Values): boolean {
if (p.view.name !== q.view.name) return false;
for (const key in p.view.params) {
if ((p.view.params as any)[key] !== (q.view.params as any)[key]) return false;
}
return true;
}
export function detailsEqual(p: Values, q: Values): boolean {
switch (p.view.name) {
case 'off':
return q.view.name === 'off';
case 'select':
return q.view.name === 'select' && p.view.params.baseDetail === q.view.params.baseDetail && p.view.params.focusDetail === q.view.params.focusDetail;
case 'all':
return q.view.name === 'all' && p.view.params.detail === q.view.params.detail;
default:
throw new Error('Not implemented');
}
}
}
export interface VisualInfo {
tag: string, // e.g. high-2, low-1 // ? remove if can be omitted
segmentId: number, // ? remove if unused
segmentName: string, // ? remove if unused
detailType: VisualInfo.DetailType, // ? remove if unused
detail: number, // ? remove if unused
color: Color, // move to MeshlistData?
visible: boolean,
data?: MeshlistData,
}
export namespace VisualInfo {
export type DetailType = 'low' | 'high';
export const DetailTypes: DetailType[] = ['low', 'high'];
export function tagFor(segmentId: number, detail: DetailType) {
return `${detail}-${segmentId}`;
}
}
export class Behavior extends PluginBehavior.WithSubscribers<Params.Values> {
private id: string;
private ref: string = '';
public parentData: MeshServerInfo.Data;
private metadata?: MetadataWrapper;
public visuals?: { [tag: string]: VisualInfo };
public backgroundSegments: { [segmentId: number]: boolean } = {};
private focusObservable = this.plugin.behaviors.interaction.click.pipe( // QUESTION is this OK way to get focused segment?
map(evt => evt.current.loci),
map(loci => (loci.kind === 'group-loci') ? loci.shape.sourceData as MeshlistData : null),
map(data => (data?.ownerId === this.id) ? data : null), // do not process shapes created by others
distinctUntilChanged((old, current) => old?.segmentId === current?.segmentId),
);
private focusSubscription?: PluginCommand.Subscription = undefined;
private backgroundSegmentsInitialized = false;
constructor(plugin: PluginContext, data: MeshServerInfo.Data, params: Params.Values) {
super(plugin, params);
this.id = UUID.create22();
this.parentData = data;
}
register(ref: string): void {
this.ref = ref;
}
unregister(): void {
if (this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
// TODO empty cache here (if used)
}
selectSegment(segmentId: number) {
if (this.params.view.name === 'select') {
if (this.params.view.params.selectedSegment === segmentId) return;
const newParams = Params.copyValues(this.params);
if (newParams.view.name === 'select') {
newParams.view.params.selectedSegment = segmentId;
}
const state = this.plugin.state.data;
const update = state.build().to(this.ref).update(newParams);
PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async update(params: Params.Values) {
const oldParams = this.params;
this.params = params;
if (!this.metadata) {
const response = await fetch(this.getMetadataUrl());
const rawMetadata = await response.json();
this.metadata = new MetadataWrapper(rawMetadata);
}
if (!this.visuals) {
this.initVisualInfos();
} else if (!Params.detailsEqual(this.params, oldParams)) {
this.updateVisualInfoDetails();
}
switch (params.view.name) {
case 'off':
await this.disableVisuals();
break;
case 'select':
await this.enableVisuals(params.view.params.selectedSegment);
break;
case 'all':
await this.enableVisuals();
break;
default:
throw new Error('Not implemented');
}
if (params.view.name !== 'off' && !this.backgroundSegmentsInitialized) {
this.guessBackgroundSegments();
this.backgroundSegmentsInitialized = true;
}
if (params.view.name === 'select' && !this.focusSubscription) {
this.focusSubscription = this.subscribeObservable(this.focusObservable, data => { this.selectSegment(data?.segmentId ?? NO_SEGMENT); });
} else if (params.view.name !== 'select' && this.focusSubscription) {
this.focusSubscription.unsubscribe();
this.focusSubscription = undefined;
}
return true;
}
private getMetadataUrl() {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/metadata`;
}
private getMeshUrl(segment: number, detail: number) {
return `${this.parentData.serverUrl}/${this.parentData.source}/${this.parentData.entryId}/mesh_bcif/${segment}/${detail}`;
}
private initVisualInfos() {
const visuals: { [tag: string]: VisualInfo } = {};
for (const segid of this.metadata!.meshSegmentIds) {
const name = this.metadata?.getSegment(segid)?.biological_annotation.name ?? DEFAULT_SEGMENT_NAME;
const color = this.metadata?.getSegmentColor(segid) ?? DEFAULT_SEGMENT_COLOR;
for (const detailType of VisualInfo.DetailTypes) {
const visual: VisualInfo = {
tag: VisualInfo.tagFor(segid, detailType),
segmentId: segid,
segmentName: name,
detailType: detailType,
detail: -1, // to be set at the end
color: color,
visible: false,
data: undefined,
};
visuals[visual.tag] = visual;
}
}
this.visuals = visuals;
this.updateVisualInfoDetails();
}
private updateVisualInfoDetails() {
let highDetail: number | undefined;
let lowDetail: number | undefined;
switch (this.params.view.name) {
case 'off':
lowDetail = undefined;
highDetail = undefined;
break;
case 'select':
lowDetail = this.params.view.params.baseDetail;
highDetail = this.params.view.params.focusDetail;
break;
case 'all':
lowDetail = this.params.view.params.detail;
highDetail = undefined;
break;
}
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const preferredDetail = (visual.detailType === 'high') ? highDetail : lowDetail;
if (preferredDetail !== undefined) {
visual.detail = this.metadata!.getSufficientMeshDetail(visual.segmentId, preferredDetail);
}
}
}
private async enableVisuals(highDetailSegment?: number) {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
const requiredDetailType = visual.segmentId === highDetailSegment ? 'high' : 'low';
if (visual.detailType === requiredDetailType) {
visual.data = await this.getMeshData(visual);
visual.visible = true;
} else {
visual.visible = false;
}
}
}
private async disableVisuals() {
for (const tag in this.visuals) {
const visual = this.visuals[tag];
visual.visible = false;
}
}
/** Fetch data in current `visual.detail`, or return already fetched data (if available in the correct detail). */
private async getMeshData(visual: VisualInfo): Promise<MeshlistData> {
if (visual.data && visual.data.detail === visual.detail) {
// Do not recreate
return visual.data;
}
// TODO cache
const url = this.getMeshUrl(visual.segmentId, visual.detail);
const urlAsset = Asset.getUrlAsset(this.plugin.managers.asset, url);
const asset = await this.plugin.runTask(this.plugin.managers.asset.resolve(urlAsset, 'binary'));
const parsed = await this.plugin.runTask(CIF.parseBinary(asset.data));
if (parsed.isError) {
throw new Error(`Failed parsing CIF file from ${url}`);
}
const meshlistData = await MeshlistData.fromCIF(parsed.result, visual.segmentId, visual.segmentName, visual.detail);
meshlistData.ownerId = this.id;
// const bbox = MeshlistData.bbox(meshlistData);
// const bboxVolume = bbox ? MS.Box3D.volume(bbox) : 0.0;
// console.log(`BBox ${visual.segmentId}: ${Math.round(bboxVolume! / 1e6)} M`, bbox); // DEBUG
return meshlistData;
}
private async guessBackgroundSegments() {
const bboxes: { [segid: number]: Box3D } = {};
for (const tag in this.visuals) {
const visual = this.visuals[tag];
if (visual.detailType === 'low' && visual.data) {
const bbox = MeshlistData.bbox(visual.data);
if (bbox) {
bboxes[visual.segmentId] = bbox;
}
}
}
const totalBbox = MeshlistData.combineBBoxes(Object.values(bboxes));
const totalVolume = totalBbox ? Box3D.volume(totalBbox) : 0.0;
// console.log(`BBox total: ${Math.round(totalVolume! / 1e6)} M`, totalBbox); // DEBUG
const isBgSegment: { [segid: number]: boolean } = {};
for (const segid in bboxes) {
const bbox = bboxes[segid];
const bboxVolume = Box3D.volume(bbox);
isBgSegment[segid] = (bboxVolume > totalVolume * BACKGROUND_SEGMENT_VOLUME_THRESHOLD);
// console.log(`BBox ${segid}: ${Math.round(bboxVolume! / 1e6)} M, ${bboxVolume / totalVolume}`, bbox); // DEBUG
}
this.backgroundSegments = isBgSegment;
}
getDescription() {
return Params.ViewTypeChoice.prettyName(this.params.view.name);
}
}
}

View File

@@ -0,0 +1,28 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
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';
export class MeshServerInfo extends PluginStateObject.Create<MeshServerInfo.Data>({ name: 'Volume Server', typeClass: 'Object' }) { }
export namespace MeshServerInfo {
export const MeshSourceChoice = new Choice({ empiar: 'EMPIAR', emdb: 'EMDB' }, 'empiar');
export type MeshSource = Choice.Values<typeof MeshSourceChoice>;
export const Params = {
serverUrl: PD.Text(DEFAULT_MESH_SERVER),
source: MeshSourceChoice.PDSelect(),
entryId: PD.Text(''),
};
export type Data = PD.Values<typeof Params>;
}

View File

@@ -0,0 +1,214 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginContext } from '../../../mol-plugin/context';
import { ShapeRepresentation } from '../../../mol-repr/shape/representation';
import { StateAction, StateTransformer } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { shallowEqualObjects } from '../../../mol-util';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { BACKGROUND_OPACITY, FOREROUND_OPACITY, MeshlistData, VolsegTransform } from '../mesh-extension';
import { MeshStreaming, NO_SEGMENT } from './behavior';
import { MeshServerInfo } from './server-info';
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshServerTransformer = VolsegTransform({
name: 'mesh-server-info',
from: PluginStateObject.Root,
to: MeshServerInfo,
params: MeshServerInfo.Params,
})({
apply({ a, params }, plugin: PluginContext) { // `a` is the parent node, `params` are 2nd argument to To.apply()
params.serverUrl = params.serverUrl.replace(/\/*$/, ''); // trim trailing slash
const description: string = params.entryId;
return new MeshServerInfo({ ...params }, { label: 'Mesh Server', description: description });
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshStreamingTransformer = VolsegTransform({
name: 'mesh-streaming-from-server-info',
display: { name: 'Mesh Streaming' },
from: MeshServerInfo,
to: MeshStreaming,
params: a => MeshStreaming.Params.create(a!.data),
})({
canAutoUpdate() { return true; },
apply({ a, params }, plugin: PluginContext) {
return Task.create('Mesh Streaming', async ctx => {
const behavior = new MeshStreaming.Behavior(plugin, a.data, params);
await behavior.update(params);
return new MeshStreaming(behavior, { label: 'Mesh Streaming', description: behavior.getDescription() });
});
},
update({ a, b, oldParams, newParams }) {
return Task.create('Update Mesh Streaming', async ctx => {
if (a.data.source !== b.data.parentData.source || a.data.entryId !== b.data.parentData.entryId) {
return StateTransformer.UpdateResult.Recreate;
}
b.data.parentData = a.data;
await b.data.update(newParams);
b.description = b.data.getDescription();
return StateTransformer.UpdateResult.Updated;
});
}
});
// // // // // // // // // // // // // // // // // // // // // // // //
interface MeshVisualGroupData {
opacity: number,
}
// export type MeshVisualGroupTransformer = typeof MeshVisualGroupTransformer;
export const MeshVisualGroupTransformer = VolsegTransform({
name: 'mesh-visual-group-from-streaming',
display: { name: 'Mesh Visuals for a Segment' },
from: MeshStreaming,
to: PluginStateObject.Group,
params: {
/** Shown on the node in GUI */
label: PD.Text('', { isHidden: true }),
/** Shown on the node in GUI (gray letters) */
description: PD.Text(''),
segmentId: PD.Numeric(NO_SEGMENT, {}, { isHidden: true }),
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }),
}
})({
apply({ a, params }, plugin) {
trySetAutoOpacity(params, a);
return new PluginStateObject.Group({ opacity: params.opacity }, params);
},
update({ a, b, oldParams, newParams }, plugin) {
if (shallowEqualObjects(oldParams, newParams)) {
return StateTransformer.UpdateResult.Unchanged;
}
newParams.label ||= oldParams.label; // Protect against resetting params to invalid defaults
if (newParams.segmentId === NO_SEGMENT) newParams.segmentId = oldParams.segmentId; // Protect against resetting params to invalid defaults
trySetAutoOpacity(newParams, a);
b.label = newParams.label;
b.description = newParams.description;
(b.data as MeshVisualGroupData).opacity = newParams.opacity;
return StateTransformer.UpdateResult.Updated;
},
canAutoUpdate({ oldParams, newParams }, plugin) {
return newParams.description === oldParams.description;
},
});
function trySetAutoOpacity(params: StateTransformer.Params<typeof MeshVisualGroupTransformer>, parent: MeshStreaming) {
if (params.opacity === -1) {
const isBgSegment = parent.data.backgroundSegments[params.segmentId];
if (isBgSegment !== undefined) {
params.opacity = isBgSegment ? BACKGROUND_OPACITY : FOREROUND_OPACITY;
}
}
}
// // // // // // // // // // // // // // // // // // // // // // // //
export const MeshVisualTransformer = VolsegTransform({
name: 'mesh-visual-from-streaming',
display: { name: 'Mesh Visual from Streaming' },
from: MeshStreaming,
to: PluginStateObject.Shape.Representation3D,
params: {
/** Must be set to PluginStateObject reference to self */
ref: PD.Text('', { isHidden: true, isEssential: true }), // QUESTION what is isEssential
/** Identification of the mesh visual, e.g. 'low-2' */
tag: PD.Text('', { isHidden: true, isEssential: true }),
/** Opacity of the visual (not to be set directly, but controlled by the opacity of the parent Group, and by VisualInfo.visible) */
opacity: PD.Numeric(-1, { min: 0, max: 1, step: 0.01 }, { isHidden: true }),
}
})({
apply({ a, params, spine }, plugin: PluginContext) {
return Task.create('Mesh Visual', async ctx => {
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![params.tag];
if (!visualInfo) throw new Error(`VisualInfo with tag '${params.tag}' is missing.`);
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
params.opacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0;
const props = PD.getDefaultValues(Mesh.Params);
props.flatShaded = true; // `flatShaded: true` is to see the real mesh vertices and triangles (default: false)
props.alpha = params.opacity;
const repr = ShapeRepresentation((ctx, meshlist: MeshlistData) => MeshlistData.getShape(meshlist, visualInfo.color), Mesh.Utils);
await repr.createOrUpdate(props, visualInfo.data ?? MeshlistData.empty()).runInContext(ctx);
return new PluginStateObject.Shape.Representation3D({ repr, sourceData: visualInfo.data }, { label: 'Mesh Visual', description: params.tag });
});
},
update({ a, b, oldParams, newParams, spine }, plugin: PluginContext) {
return Task.create('Update Mesh Visual', async ctx => {
newParams.ref ||= oldParams.ref; // Protect against resetting params to invalid defaults
newParams.tag ||= oldParams.tag; // Protect against resetting params to invalid defaults
const visualInfo: MeshStreaming.VisualInfo = a.data.visuals![newParams.tag];
if (!visualInfo) throw new Error(`VisualInfo with tag '${newParams.tag}' is missing.`);
const oldData = b.data.sourceData as MeshlistData | undefined;
if (visualInfo.data?.detail !== oldData?.detail) {
return StateTransformer.UpdateResult.Recreate;
}
const groupData = spine.getAncestorOfType(PluginStateObject.Group)?.data as MeshVisualGroupData | undefined;
const newOpacity = visualInfo.visible ? (groupData?.opacity ?? FOREROUND_OPACITY) : 0.0; // do not store to newParams directly, because oldParams and newParams might point to the same object!
if (newOpacity !== oldParams.opacity) {
newParams.opacity = newOpacity;
await b.data.repr.createOrUpdate({ alpha: newParams.opacity }).runInContext(ctx);
return StateTransformer.UpdateResult.Updated;
} else {
return StateTransformer.UpdateResult.Unchanged;
}
});
},
canAutoUpdate(params, globalCtx) {
return true;
},
dispose({ b, params }, plugin) {
b?.data.repr.destroy(); // QUESTION is this correct?
},
});
// // // // // // // // // // // // // // // // // // // // // // // //
export const InitMeshStreaming = StateAction.build({
display: { name: 'Mesh Streaming' },
from: PluginStateObject.Root,
params: MeshServerInfo.Params,
isApplicable: (a, _, plugin: PluginContext) => true
})(function (p, plugin: PluginContext) {
return Task.create('Mesh Streaming', async ctx => {
const { params } = p;
// p.ref
const serverNode = await plugin.build().to(p.ref).apply(MeshServerTransformer, params).commit();
// const serverNode = await plugin.build().toRoot().apply(MeshServerTransformer, params).commit();
const streamingNode = await plugin.build().to(serverNode).apply(MeshStreamingTransformer, {}).commit();
const visuals = streamingNode.data?.visuals ?? {};
const bgSegments = streamingNode.data?.backgroundSegments ?? {};
const segmentGroups: { [segid: number]: string } = {};
for (const tag in visuals) {
const segid = visuals[tag].segmentId;
if (!segmentGroups[segid]) {
let description = visuals[tag].segmentName;
if (bgSegments[segid]) description += ' (background)';
const group = await plugin.build().to(streamingNode).apply(MeshVisualGroupTransformer, { label: `Segment ${segid}`, description: description, segmentId: segid }, { state: { isCollapsed: true } }).commit();
segmentGroups[segid] = group.ref;
}
}
const visualsUpdate = plugin.build();
for (const tag in visuals) {
const ref = `${streamingNode.ref}-${tag}`;
const segid = visuals[tag].segmentId;
visualsUpdate.to(segmentGroups[segid]).apply(MeshVisualTransformer, { ref: ref, tag: tag }, { ref: ref }); // ref - hack to allow the node make itself invisible
}
await plugin.state.data.updateTree(visualsUpdate).runInContext(ctx); // QUESTION what is really the difference between this and `visualsUpdate.commit()`?
});
});
// TODO make available in GUI, in left panel or in right panel like Volume Streaming in src/mol-plugin-ui/structure/volume.tsx?

View File

@@ -0,0 +1,340 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
/** Helper functions for manipulation with mesh data. */
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { CIF, CifFile } from '../../mol-io/reader/cif';
import { Box3D } from '../../mol-math/geometry';
import { Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { volumeFromDensityServerData } from '../../mol-model-formats/volume/density-server';
import { Grid } from '../../mol-model/volume';
import { ColorNames } from '../../mol-util/color/names';
import { TypedArray } from '../../mol-util/type-helpers';
import { CIF_schema_mesh } from './mesh-cif-schema';
type MeshModificationParams = {
scale?: [number, number, number],
shift?: [number, number, number],
matrix?: Mat4,
group?: number,
invertSides?: boolean
};
/** Modify mesh in-place */
export function modify(m: Mesh, params: MeshModificationParams) {
if (params.scale !== undefined) {
const [qx, qy, qz] = params.scale;
const vertices = m.vertexBuffer.ref.value;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] *= qx;
vertices[i + 1] *= qy;
vertices[i + 2] *= qz;
}
}
if (params.shift !== undefined) {
const [dx, dy, dz] = params.shift;
const vertices = m.vertexBuffer.ref.value;
for (let i = 0; i < vertices.length; i += 3) {
vertices[i] += dx;
vertices[i + 1] += dy;
vertices[i + 2] += dz;
}
}
if (params.matrix !== undefined) {
const r = m.vertexBuffer.ref.value;
const matrix = params.matrix;
const size = 3 * m.vertexCount;
for (let i = 0; i < size; i += 3) {
Vec3.transformMat4Offset(r, r, matrix, i, i, 0);
}
}
if (params.group !== undefined) {
const groups = m.groupBuffer.ref.value;
for (let i = 0; i < groups.length; i++) {
groups[i] = params.group;
}
}
if (params.invertSides) {
const indices = m.indexBuffer.ref.value;
let tmp;
for (let i = 0; i < indices.length; i += 3) {
tmp = indices[i];
indices[i] = indices[i + 1];
indices[i + 1] = tmp;
}
const normals = m.normalBuffer.ref.value;
for (let i = 0; i < normals.length; i++) {
normals[i] *= -1;
}
}
}
/** Create a copy a mesh, possibly modified */
export function copy(m: Mesh, modification?: MeshModificationParams): Mesh {
const nVertices = m.vertexCount;
const nTriangles = m.triangleCount;
const vertices = new Float32Array(m.vertexBuffer.ref.value);
const indices = new Uint32Array(m.indexBuffer.ref.value);
const normals = new Float32Array(m.normalBuffer.ref.value);
const groups = new Float32Array(m.groupBuffer.ref.value);
const result = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
if (modification) {
modify(result, modification);
}
return result;
}
/** Join more meshes into one */
export function concat(...meshes: Mesh[]): Mesh {
const nVertices = sum(meshes.map(m => m.vertexCount));
const nTriangles = sum(meshes.map(m => m.triangleCount));
const vertices = concatArrays(Float32Array, meshes.map(m => m.vertexBuffer.ref.value));
const normals = concatArrays(Float32Array, meshes.map(m => m.normalBuffer.ref.value));
const groups = concatArrays(Float32Array, meshes.map(m => m.groupBuffer.ref.value));
const newIndices = [];
let offset = 0;
for (const m of meshes) {
newIndices.push(m.indexBuffer.ref.value.map(i => i + offset));
offset += m.vertexCount;
}
const indices = concatArrays(Uint32Array, newIndices);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Return Mesh from CIF data and mesh IDs (group IDs).
* Assume the CIF contains coords in grid space,
* transform the output mesh to `space` */
export async function meshFromCif(data: CifFile, invertSides: boolean | undefined = undefined, outSpace: 'grid' | 'fractional' | 'cartesian' = 'cartesian'): Promise<{ mesh: Mesh, meshIds: number[] }> {
const volumeInfoBlock = data.blocks.find(b => b.header === 'VOLUME_INFO');
const meshesBlock = data.blocks.find(b => b.header === 'MESHES');
if (!volumeInfoBlock || !meshesBlock) throw new Error('Missing VOLUME_INFO or MESHES block in mesh CIF file');
const volumeInfoCif = CIF.schema.densityServer(volumeInfoBlock);
const meshCif = CIF_schema_mesh(meshesBlock);
const nVertices = meshCif.mesh_vertex._rowCount;
const nTriangles = Math.floor(meshCif.mesh_triangle._rowCount / 3);
const mesh_id = meshCif.mesh.id.toArray();
const vertex_meshId = meshCif.mesh_vertex.mesh_id.toArray();
const x = meshCif.mesh_vertex.x.toArray();
const y = meshCif.mesh_vertex.y.toArray();
const z = meshCif.mesh_vertex.z.toArray();
const triangle_meshId = meshCif.mesh_triangle.mesh_id.toArray();
const triangle_vertexId = meshCif.mesh_triangle.vertex_id.toArray();
// Shift indices from within-mesh indices to overall indices
const indices = new Uint32Array(3 * nTriangles);
const offsets = offsetMap(vertex_meshId);
for (let i = 0; i < 3 * nTriangles; i++) {
const offset = offsets.get(triangle_meshId[i])!;
indices[i] = offset + triangle_vertexId[i];
}
const vertices = flattenCoords(x, y, z);
const normals = new Float32Array(3 * nVertices);
const groups = new Float32Array(vertex_meshId);
const mesh = Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
invertSides ??= isInverted(mesh);
if (invertSides) {
modify(mesh, { invertSides: true }); // Vertex orientation convention is opposite in Volseg API and in MolStar
}
if (outSpace === 'cartesian') {
const volume = await volumeFromDensityServerData(volumeInfoCif).run();
const gridToCartesian = Grid.getGridToCartesianTransform(volume.grid);
modify(mesh, { matrix: gridToCartesian });
} else if (outSpace === 'fractional') {
const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0);
const originFract = volumeInfoCif.volume_data_3d_info.origin.value(0);
const dimensionFract = volumeInfoCif.volume_data_3d_info.dimensions.value(0);
if (dimensionFract[0] !== 1 || dimensionFract[1] !== 1 || dimensionFract[2] !== 1) throw new Error(`Asserted the fractional dimensions are [1,1,1], but are actually [${dimensionFract}]`);
const scale: [number, number, number] = [1 / gridSize[0], 1 / gridSize[1], 1 / gridSize[2]];
modify(mesh, { scale: scale, shift: Array.from(originFract) as any });
}
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
// const boxMesh = makeMeshFromBox([[0,0,0], [1,1,1]], 1);
// const gridSize = volumeInfoCif.volume_data_3d_info.sample_count.value(0); const boxMesh = makeMeshFromBox([[0,0,0], Array.from(gridSize)] as any, 1);
// const cellSize = volumeInfoCif.volume_data_3d_info.spacegroup_cell_size.value(0); const boxMesh = makeMeshFromBox([[0, 0, 0], Array.from(cellSize)] as any, 1);
// mesh = concat(mesh, boxMesh); // debug
return { mesh: mesh, meshIds: Array.from(mesh_id) };
}
function isInverted(mesh: Mesh): boolean {
const vertices = mesh.vertexBuffer.ref.value;
const indices = mesh.indexBuffer.ref.value;
const center = meshCenter(mesh);
const center3 = Vec3.create(3 * center[0], 3 * center[1], 3 * center[2]);
let dirMetric = 0.0;
const [a, b, c, u, v, normal, radius] = [Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3(), Vec3()];
for (let i = 0; i < indices.length; i += 3) {
Vec3.fromArray(a, vertices, 3 * indices[i]);
Vec3.fromArray(b, vertices, 3 * indices[i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[i + 2]);
Vec3.sub(u, b, a);
Vec3.sub(v, c, b);
Vec3.cross(normal, u, v); // direction of the surface
Vec3.add(radius, a, b);
Vec3.add(radius, radius, c);
Vec3.sub(radius, radius, center3); // direction center -> this triangle
dirMetric += Vec3.dot(radius, normal);
}
return dirMetric < 0;
}
function meshCenter(mesh: Mesh) {
const vertices = mesh.vertexBuffer.ref.value;
const n = vertices.length;
let x = 0.0;
let y = 0.0;
let z = 0.0;
for (let i = 0; i < vertices.length; i += 3) {
x += vertices[i];
y += vertices[i + 1];
z += vertices[i + 2];
}
return Vec3.create(x / n, y / n, z / n);
}
function flattenCoords(x: ArrayLike<number>, y: ArrayLike<number>, z: ArrayLike<number>): Float32Array {
const n = x.length;
const out = new Float32Array(3 * n);
for (let i = 0; i < n; i++) {
out[3 * i] = x[i];
out[3 * i + 1] = y[i];
out[3 * i + 2] = z[i];
}
return out;
}
/** Get mapping of unique values to the position of their first occurrence */
function offsetMap(values: ArrayLike<number>) {
const result = new Map<number, number>();
for (let i = 0; i < values.length; i++) {
if (!result.has(values[i])) {
result.set(values[i], i);
}
}
return result;
}
/** Return bounding box */
export function bbox(mesh: Mesh): Box3D | null { // Is there no function for this?
const nVertices = mesh.vertexCount;
const coords = mesh.vertexBuffer.ref.value;
if (nVertices === 0) {
return null;
}
let minX = coords[0], minY = coords[1], minZ = coords[2];
let maxX = minX, maxY = minY, maxZ = minZ;
for (let i = 0; i < 3 * nVertices; i += 3) {
const x = coords[i], y = coords[i + 1], z = coords[i + 2];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (z < minZ) minZ = z;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
if (z > maxZ) maxZ = z;
}
return Box3D.create(Vec3.create(minX, minY, minZ), Vec3.create(maxX, maxY, maxZ));
}
/** Example mesh - 1 triangle */
export function fakeFakeMesh1(): Mesh {
const nVertices = 3;
const nTriangles = 1;
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
const indices = new Uint32Array([0, 1, 2]);
const normals = new Float32Array([0, 0, 1]);
const groups = new Float32Array([0]);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Example mesh - irregular tetrahedron */
export function fakeMesh4(): Mesh {
const nVertices = 4;
const nTriangles = 4;
const vertices = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
const indices = new Uint32Array([0, 2, 1, 0, 1, 3, 1, 2, 3, 2, 0, 3]);
const normals = new Float32Array([-1, -1, -1, 1, 0, 0, 0, 1, 0, 0, 0, 1]);
const groups = new Float32Array([0, 1, 2, 3]);
return Mesh.create(vertices, indices, normals, groups, nVertices, nTriangles);
}
/** Return a box-shaped mesh */
export function meshFromBox(box: [[number, number, number], [number, number, number]], group: number = 0) {
const [[x0, y0, z0], [x1, y1, z1]] = box;
const vertices = new Float32Array([
x0, y0, z0,
x1, y0, z0,
x0, y1, z0,
x1, y1, z0,
x0, y0, z1,
x1, y0, z1,
x0, y1, z1,
x1, y1, z1,
]);
const indices = new Uint32Array([
2, 1, 0, 1, 2, 3,
1, 4, 0, 4, 1, 5,
3, 5, 1, 5, 3, 7,
2, 7, 3, 7, 2, 6,
0, 6, 2, 6, 0, 4,
4, 7, 6, 7, 4, 5,
]);
const groups = new Float32Array([group, group, group, group, group, group, group, group]);
const normals = new Float32Array(8);
const mesh = Mesh.create(vertices, indices, normals, groups, 8, 12);
Mesh.computeNormals(mesh); // normals only necessary if flatShaded==false
return mesh;
}
function sum(array: number[]): number {
return array.reduce((a, b) => a + b, 0);
}
function concatArrays<T extends TypedArray>(t: new (len: number) => T, arrays: T[]): T {
const totalLength = arrays.map(a => a.length).reduce((a, b) => a + b, 0);
const result: T = new t(totalLength);
let offset = 0;
for (const array of arrays) {
result.set(array, offset);
offset += array.length;
}
return result;
}
/** Generate random colors (in a cycle) */
export const ColorGenerator = function* () {
const colors = shuffleArray(Object.values(ColorNames));
let i = 0;
while (true) {
yield colors[i];
i++;
if (i >= colors.length) i = 0;
}
}();
function shuffleArray<T>(array: T[]): T[] {
// Stealed from https://www.w3docs.com/snippets/javascript/how-to-randomize-shuffle-a-javascript-array.html
let curId = array.length;
// There remain elements to shuffle
while (0 !== curId) {
// Pick a remaining element
const randId = Math.floor(Math.random() * curId);
curId -= 1;
// Swap it with the current element.
const tmp = array[curId];
array[curId] = array[randId];
array[randId] = tmp;
}
return array;
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -14,6 +15,7 @@ import { CustomPropSymbol } from '../../../mol-script/language/symbol';
import { Type } from '../../../mol-script/language/type';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
export { QualityAssessment };
@@ -71,14 +73,28 @@ namespace QualityAssessment {
localNames.set(ma_qa_metric.id.value(i), name);
}
const residueKey: AtomicIndex.ResidueLabelKey = {
label_entity_id: '',
label_asym_id: '',
label_seq_id: 0,
pdbx_PDB_ins_code: undefined,
};
for (let i = 0, il = ma_qa_metric_local._rowCount; i < il; i++) {
if (model_id.value(i) !== model.modelNum) continue;
const labelAsymId = label_asym_id.value(i);
const entityIndex = index.findEntity(labelAsymId);
const rI = index.findResidue(model.entities.data.id.value(entityIndex), labelAsymId, label_seq_id.value(i));
const name = localNames.get(metric_id.value(i))!;
localMetrics.get(name)!.set(rI, metric_value.value(i));
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
residueKey.label_asym_id = labelAsymId;
residueKey.label_seq_id = label_seq_id.value(i);
const rI = index.findResidueLabel(residueKey);
if (rI >= 0) {
const name = localNames.get(metric_id.value(i))!;
localMetrics.get(name)!.set(rI, metric_value.value(i));
}
}
return {

View File

@@ -29,7 +29,7 @@ export { StructureQualityReport };
type StructureQualityReport = PropertyWrapper<{
issues: IndexedCustomProperty.Residue<string[]>,
issueTypes: string[]
}| undefined>
} | undefined>
namespace StructureQualityReport {
export const DefaultServerUrl = 'https://www.ebi.ac.uk/pdbe/api/validation/residuewise_outlier_summary/entry/';

File diff suppressed because it is too large Load Diff

View 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.

View File

@@ -0,0 +1,3 @@
export { SbNcbrPartialCharges } from './partial-charges/behavior';
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';

View 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),
}),
});

View 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),
},
};

View 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'),
};
}

View 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
);
},
});

View 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)),
});

View File

@@ -0,0 +1,103 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { ShapeRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { PluginCommands } from '../../mol-plugin/commands';
import { Color } from '../../mol-util/color';
import { ColorNames } from '../../mol-util/color/names';
import { BACKGROUND_SEGMENT_VOLUME_THRESHOLD } from '../meshes/mesh-streaming/behavior';
import { createMeshFromUrl } from '../meshes/mesh-extension';
import { Segment } from './volseg-api/data';
import { VolsegEntryData } from './entry-root';
const DEFAULT_MESH_DETAIL: number | null = 5; // null means worst
export class VolsegMeshSegmentationData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadSegmentation() {
const hasMeshes = this.entryData.metadata.meshSegmentIds.length > 0;
if (hasMeshes) {
await this.showSegments(this.entryData.metadata.allSegmentIds);
}
}
updateOpacity(opacity: number) {
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
const update = this.entryData.newUpdate();
for (const visual of visuals) {
update.to(visual).update(ShapeRepresentation3D, p => { (p as any).alpha = opacity; });
}
return update.commit();
}
async highlightSegment(segment: Segment) {
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment.id}`);
for (const visual of visuals) {
await PluginCommands.Interactivity.Object.Highlight(this.entryData.plugin, { state: this.entryData.plugin.state.data, ref: visual.transform.ref });
}
}
async selectSegment(segment?: number) {
if (segment === undefined || segment < 0) return;
const visuals = this.entryData.findNodesByTags('mesh-segment-visual', `segment-${segment}`);
const reprNode: PluginStateObject.Shape.Representation3D | undefined = visuals[0]?.obj;
if (!reprNode) return;
const loci = reprNode.data.repr.getAllLoci()[0];
if (!loci) return;
this.entryData.plugin.managers.interactivity.lociSelects.select({ loci: loci, repr: reprNode.data.repr }, false);
}
/** Make visible the specified set of mesh segments */
async showSegments(segments: number[]) {
const segmentsToShow = new Set(segments);
const visuals = this.entryData.findNodesByTags('mesh-segment-visual');
for (const visual of visuals) {
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('segment-'));
if (!theTag) continue;
const id = parseInt(theTag.split('-')[1]);
const visibility = segmentsToShow.has(id);
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
segmentsToShow.delete(id);
}
const segmentsToCreate = this.entryData.metadata.meshSegmentIds.filter(seg => segmentsToShow.has(seg));
if (segmentsToCreate.length === 0) return;
let group = this.entryData.findNodesByTags('mesh-segmentation-group')[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Segmentation', description: 'Mesh' }, { tags: ['mesh-segmentation-group'], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const totalVolume = this.entryData.metadata.gridTotalVolume;
const awaiting = [];
for (const seg of segmentsToCreate) {
const segment = this.entryData.metadata.getSegment(seg);
if (!segment) continue;
const detail = this.entryData.metadata.getSufficientMeshDetail(seg, DEFAULT_MESH_DETAIL);
const color = segment.colour.length >= 3 ? Color.fromNormalizedArray(segment.colour, 0) : ColorNames.gray;
const url = this.entryData.api.meshUrl_Bcif(this.entryData.source, this.entryData.entryId, seg, detail);
const label = segment.biological_annotation.name ?? `Segment ${seg}`;
const meshPromise = createMeshFromUrl(this.entryData.plugin, url, seg, detail, true, color, group,
BACKGROUND_SEGMENT_VOLUME_THRESHOLD * totalVolume, `<b>${label}</b>`, this.entryData.ref);
awaiting.push(meshPromise);
}
for (const promise of awaiting) await promise;
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2018-2022 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 { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { TrajectoryFromMmCif } from '../../mol-plugin-state/transforms/model';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { StateObjectRef, StateObjectSelector } from '../../mol-state';
import { VolsegEntryData } from './entry-root';
export class VolsegModelData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
private async loadPdb(pdbId: string, parent: StateObjectSelector | StateObjectRef) {
const url = `https://www.ebi.ac.uk/pdbe/entry-files/download/${pdbId}.bcif`;
const dataNode = await this.entryData.plugin.build().to(parent).apply(Download, { url: url, isBinary: true }, { tags: ['fitted-model-data', `pdbid-${pdbId}`] }).commit();
const cifNode = await this.entryData.plugin.build().to(dataNode).apply(ParseCif).commit();
const trajectoryNode = await this.entryData.plugin.build().to(cifNode).apply(TrajectoryFromMmCif).commit();
await this.entryData.plugin.builders.structure.hierarchy.applyPreset(trajectoryNode, 'default', { representationPreset: 'polymer-cartoon' });
return dataNode;
}
async showPdbs(pdbIds: string[]) {
const segmentsToShow = new Set(pdbIds);
const visuals = this.entryData.findNodesByTags('fitted-model-data');
for (const visual of visuals) {
const theTag = visual.obj?.tags?.find(tag => tag.startsWith('pdbid-'));
if (!theTag) continue;
const id = theTag.split('-')[1];
const visibility = segmentsToShow.has(id);
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, !visibility); // true means hide, ¯\_(ツ)_/¯
segmentsToShow.delete(id);
}
const segmentsToCreate = Array.from(segmentsToShow);
if (segmentsToCreate.length === 0) return;
let group = this.entryData.findNodesByTags('fitted-models-group')[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Fitted Models' }, { tags: ['fitted-models-group'], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const awaiting = [];
for (const pdbId of segmentsToCreate) {
awaiting.push(this.loadPdb(pdbId, group));
}
for (const promise of awaiting) await promise;
}
}

View File

@@ -0,0 +1,377 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { BehaviorSubject, distinctUntilChanged, Subject, throttleTime } from 'rxjs';
import { VolsegVolumeServerConfig } from '.';
import { Loci } from '../../mol-model/loci';
import { ShapeGroup } from '../../mol-model/shape';
import { Volume } from '../../mol-model/volume';
import { LociLabelProvider } from '../../mol-plugin-state/manager/loci-label';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
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 { ParamDefinition } from '../../mol-util/param-definition';
import { MeshlistData } from '../meshes/mesh-extension';
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
import { Segment } from './volseg-api/data';
import { MetadataWrapper } from './volseg-api/utils';
import { VolsegMeshSegmentationData } from './entry-meshes';
import { VolsegModelData } from './entry-models';
import { VolsegLatticeSegmentationData } from './entry-segmentation';
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 { type VolsegStateFromEntry } from './transformers';
import { StateTransforms } from '../../mol-plugin-state/transforms';
export const MAX_VOXELS = 10 ** 7;
// export const MAX_VOXELS = 10 ** 2; // DEBUG
export const BOX: [[number, number, number], [number, number, number]] | null = null;
// export const BOX: [[number, number, number], [number, number, number]] | null = [[-90, -90, -90], [90, 90, 90]]; // DEBUG
const MAX_ANNOTATIONS_IN_LABEL = 6;
const SourceChoice = new Choice({ emdb: 'EMDB', empiar: 'EMPIAR', idr: 'IDR' }, 'emdb');
export type Source = Choice.Values<typeof SourceChoice>;
export function createLoadVolsegParams(plugin?: PluginContext, entrylists: { [source: string]: string[] } = {}) {
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
return {
serverUrl: ParamDefinition.Text(defaultVolumeServer),
source: ParamDefinition.Mapped(SourceChoice.values[0], SourceChoice.options, src => entryParam(entrylists[src])),
};
}
function entryParam(entries: string[] = []) {
const options: [string, string][] = entries.map(e => [e, e]);
options.push(['__custom__', 'Custom']);
return ParamDefinition.Group({
entryId: ParamDefinition.Select(options[0][0], options, { description: 'Choose an entry from the list, or choose "Custom" and type any entry ID (useful when using other than default server).' }),
customEntryId: ParamDefinition.Text('', { hideIf: p => p.entryId !== '__custom__', description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
}, { isFlat: true });
}
type LoadVolsegParamValues = ParamDefinition.Values<ReturnType<typeof createLoadVolsegParams>>;
export function createVolsegEntryParams(plugin?: PluginContext) {
const defaultVolumeServer = plugin?.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
return {
serverUrl: ParamDefinition.Text(defaultVolumeServer),
source: SourceChoice.PDSelect(),
entryId: ParamDefinition.Text('emd-1832', { description: 'Entry identifier, including the source prefix, e.g. "emd-1832"' }),
};
}
type VolsegEntryParamValues = ParamDefinition.Values<ReturnType<typeof createVolsegEntryParams>>;
export namespace VolsegEntryParamValues {
export function fromLoadVolsegParamValues(params: LoadVolsegParamValues): VolsegEntryParamValues {
let entryId = (params.source.params as any).entryId;
if (entryId === '__custom__') {
entryId = (params.source.params as any).customEntryId;
}
return {
serverUrl: params.serverUrl,
source: params.source.name as Source,
entryId: entryId
};
}
}
export class VolsegEntry extends PluginStateObject.CreateBehavior<VolsegEntryData>({ name: 'Vol & Seg Entry' }) { }
type VolRepr3DT = typeof StateTransforms.Representation.VolumeRepresentation3D
export class VolsegEntryData extends PluginBehavior.WithSubscribers<VolsegEntryParamValues> {
plugin: PluginContext;
ref: string = '';
api: VolumeApiV2;
source: Source;
/** Number part of entry ID; e.g. '1832' */
entryNumber: string;
/** Full entry ID; e.g. 'emd-1832' */
entryId: string;
metadata: MetadataWrapper;
pdbs: string[];
public readonly volumeData = new VolsegVolumeData(this);
private readonly latticeSegmentationData = new VolsegLatticeSegmentationData(this);
private readonly meshSegmentationData = new VolsegMeshSegmentationData(this);
private readonly modelData = new VolsegModelData(this);
private highlightRequest = new Subject<Segment | undefined>();
private getStateNode = lazyGetter(() => this.plugin.state.data.selectQ(q => q.byRef(this.ref).subtree().ofType(VolsegState))[0] as StateObjectCell<VolsegState, StateTransform<typeof VolsegStateFromEntry>>, 'Missing VolsegState node. Must first create VolsegState for this VolsegEntry.');
public currentState = new BehaviorSubject(ParamDefinition.getDefaultValues(VolsegStateParams));
public currentVolume = new BehaviorSubject<StateTransform<VolRepr3DT> | undefined>(undefined);
private constructor(plugin: PluginContext, params: VolsegEntryParamValues) {
super(plugin, params);
this.plugin = plugin;
this.api = new VolumeApiV2(params.serverUrl);
this.source = params.source;
this.entryId = params.entryId;
this.entryNumber = splitEntryId(this.entryId).entryNumber;
}
private async initialize() {
const metadata = await this.api.getMetadata(this.source, this.entryId);
this.metadata = new MetadataWrapper(metadata);
this.pdbs = await ExternalAPIs.getPdbIdsForEmdbEntry(this.metadata.raw.grid.general.source_db_id ?? this.entryId);
// TODO use Asset?
}
static async create(plugin: PluginContext, params: VolsegEntryParamValues) {
const result = new VolsegEntryData(plugin, params);
await result.initialize();
return result;
}
async register(ref: string) {
this.ref = ref;
this.plugin.managers.lociLabels.addProvider(this.labelProvider);
try {
const params = this.getStateNode().obj?.data;
if (params) {
this.currentState.next(params);
}
} catch {
// do nothing
}
const volumeVisual = this.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (volumeVisual) this.currentVolume.next(volumeVisual.transform);
let volumeRef: string | undefined;
this.subscribeObservable(this.plugin.state.data.events.cell.stateUpdated, e => {
try { (this.getStateNode()); } catch { return; } // if state not does not exist yet
if (e.cell.transform.ref === this.getStateNode().transform.ref) {
const newState = this.getStateNode().obj?.data;
if (newState && !shallowEqualObjects(newState, this.currentState.value)) { // avoid repeated update
this.currentState.next(newState);
}
} else if (e.cell.transform.tags?.includes(VOLUME_VISUAL_TAG)) {
if (e.ref === volumeRef) {
this.currentVolume.next(e.cell.transform);
} else if (StateSelection.findAncestor(this.plugin.state.data.tree, this.plugin.state.data.cells, e.ref, a => a.transform.ref === ref)) {
volumeRef = e.ref;
this.currentVolume.next(e.cell.transform);
}
}
});
this.subscribeObservable(this.plugin.state.data.events.cell.removed, e => {
if (e.ref === volumeRef) {
volumeRef = undefined;
this.currentVolume.next(undefined);
}
});
this.subscribeObservable(this.plugin.behaviors.interaction.click, async e => {
const loci = e.current.loci;
const clickedSegment = this.getSegmentIdFromLoci(loci);
if (clickedSegment === undefined) return;
if (clickedSegment === this.currentState.value.selectedSegment) {
this.actionSelectSegment(undefined);
} else {
this.actionSelectSegment(clickedSegment);
}
});
this.subscribeObservable(
this.highlightRequest.pipe(throttleTime(50, undefined, { leading: true, trailing: true })),
async segment => await this.highlightSegment(segment)
);
this.subscribeObservable(
this.currentState.pipe(distinctUntilChanged((a, b) => a.selectedSegment === b.selectedSegment)),
async state => {
if (VolsegGlobalStateData.getGlobalState(this.plugin)?.selectionMode) await this.selectSegment(state.selectedSegment);
}
);
}
async unregister() {
this.plugin.managers.lociLabels.removeProvider(this.labelProvider);
}
async loadVolume() {
const result = await this.volumeData.loadVolume();
if (result) {
const isovalue = result.isovalue.kind === 'relative' ? result.isovalue.relativeValue : result.isovalue.absoluteValue;
await this.updateStateNode({ volumeIsovalueKind: result.isovalue.kind, volumeIsovalueValue: isovalue });
}
}
async loadSegmentations() {
await this.latticeSegmentationData.loadSegmentation();
await this.meshSegmentationData.loadSegmentation();
await this.actionShowSegments(this.metadata.allSegmentIds);
}
actionHighlightSegment(segment?: Segment) {
this.highlightRequest.next(segment);
}
async actionToggleSegment(segment: number) {
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
if (current.includes(segment)) {
await this.actionShowSegments(current.filter(s => s !== segment));
} else {
await this.actionShowSegments([...current, segment]);
}
}
async actionToggleAllSegments() {
const current = this.currentState.value.visibleSegments.map(seg => seg.segmentId);
if (current.length !== this.metadata.allSegments.length) {
await this.actionShowSegments(this.metadata.allSegmentIds);
} else {
await this.actionShowSegments([]);
}
}
async actionSelectSegment(segment?: number) {
if (segment !== undefined && this.currentState.value.visibleSegments.find(s => s.segmentId === segment) === undefined) {
// first make the segment visible if it is not
await this.actionToggleSegment(segment);
}
await this.updateStateNode({ selectedSegment: segment });
}
async actionSetOpacity(opacity: number) {
if (opacity === this.getStateNode().obj?.data.segmentOpacity) return;
this.latticeSegmentationData.updateOpacity(opacity);
this.meshSegmentationData.updateOpacity(opacity);
await this.updateStateNode({ segmentOpacity: opacity });
}
async actionShowFittedModel(pdbIds: string[]) {
await this.modelData.showPdbs(pdbIds);
await this.updateStateNode({ visibleModels: pdbIds.map(pdbId => ({ pdbId: pdbId })) });
}
async actionSetVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
await this.volumeData.setVolumeVisual(type);
await this.updateStateNode({ volumeType: type });
}
async actionUpdateVolumeVisual(params: SimpleVolumeParamValues) {
await this.volumeData.updateVolumeVisual(params);
await this.updateStateNode({
volumeType: params.volumeType,
volumeOpacity: params.opacity,
});
}
private async actionShowSegments(segments: number[]) {
await this.latticeSegmentationData.showSegments(segments);
await this.meshSegmentationData.showSegments(segments);
await this.updateStateNode({ visibleSegments: segments.map(s => ({ segmentId: s })) });
}
private async highlightSegment(segment?: Segment) {
await PluginCommands.Interactivity.ClearHighlights(this.plugin);
if (segment) {
await this.latticeSegmentationData.highlightSegment(segment);
await this.meshSegmentationData.highlightSegment(segment);
}
}
private async selectSegment(segment: number) {
this.plugin.managers.interactivity.lociSelects.deselectAll();
await this.latticeSegmentationData.selectSegment(segment);
await this.meshSegmentationData.selectSegment(segment);
await this.highlightSegment();
}
private async updateStateNode(params: Partial<VolsegStateData>) {
const oldParams = this.getStateNode().transform.params;
const newParams = { ...oldParams, ...params };
const state = this.plugin.state.data;
const update = state.build().to(this.getStateNode().transform.ref).update(newParams);
await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
}
/** Find the nodes under this entry root which have all of the given tags. */
findNodesByTags(...tags: string[]) {
return this.plugin.state.data.selectQ(q => {
let builder = q.byRef(this.ref).subtree();
for (const tag of tags) builder = builder.withTag(tag);
return builder;
});
}
newUpdate() {
if (this.ref !== '') {
return this.plugin.build().to(this.ref);
} else {
return this.plugin.build().toRoot();
}
}
private readonly labelProvider: LociLabelProvider = {
label: (loci: Loci): string | undefined => {
const segmentId = this.getSegmentIdFromLoci(loci);
if (segmentId === undefined) return;
const segment = this.metadata.getSegment(segmentId);
if (!segment) return;
const annotLabels = segment.biological_annotation.external_references.map(annot => `${applyEllipsis(annot.label)} [${annot.resource}:${annot.accession}]`);
if (annotLabels.length === 0) return;
if (annotLabels.length > MAX_ANNOTATIONS_IN_LABEL + 1) {
const nHidden = annotLabels.length - MAX_ANNOTATIONS_IN_LABEL;
annotLabels.length = MAX_ANNOTATIONS_IN_LABEL;
annotLabels.push(`(${nHidden} more annotations, click on the segment to see all)`);
}
return '<hr class="msp-highlight-info-hr"/>' + annotLabels.filter(isDefined).join('<br/>');
}
};
private getSegmentIdFromLoci(loci: Loci): number | undefined {
if (Volume.Segment.isLoci(loci) && loci.volume._propertyData.ownerId === this.ref) {
if (loci.segments.length === 1) {
return loci.segments[0];
}
}
if (ShapeGroup.isLoci(loci)) {
const meshData = (loci.shape.sourceData ?? {}) as MeshlistData;
if (meshData.ownerId === this.ref && meshData.segmentId !== undefined) {
return meshData.segmentId;
}
}
}
async setTryUseGpu(tryUseGpu: boolean) {
await Promise.all([
this.volumeData.setTryUseGpu(tryUseGpu),
this.latticeSegmentationData.setTryUseGpu(tryUseGpu),
]);
}
async setSelectionMode(selectSegments: boolean) {
if (selectSegments) {
await this.selectSegment(this.currentState.value.selectedSegment);
} else {
this.plugin.managers.interactivity.lociSelects.deselectAll();
}
}
}

View File

@@ -0,0 +1,131 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Volume } from '../../mol-model/volume';
import { createVolumeRepresentationParams } from '../../mol-plugin-state/helpers/volume-representation-params';
import { StateTransforms } from '../../mol-plugin-state/transforms';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { VolumeFromSegmentationCif } from '../../mol-plugin-state/transforms/volume';
import { PluginCommands } from '../../mol-plugin/commands';
import { Color } from '../../mol-util/color';
import { Segment } from './volseg-api/data';
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
import { VolumeVisualParams } from './entry-volume';
import { VolsegGlobalStateData } from './global-state';
const GROUP_TAG = 'lattice-segmentation-group';
const SEGMENT_VISUAL_TAG = 'lattice-segment-visual';
const DEFAULT_SEGMENT_COLOR = Color.fromNormalizedRgb(0.8, 0.8, 0.8);
export class VolsegLatticeSegmentationData {
private entryData: VolsegEntryData;
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadSegmentation() {
const hasLattices = this.entryData.metadata.raw.grid.segmentation_lattices.segmentation_lattice_ids.length > 0;
if (hasLattices) {
const url = this.entryData.api.latticeUrl(this.entryData.source, this.entryData.entryId, 0, BOX, MAX_VOXELS);
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup,
{ label: 'Segmentation', description: 'Lattice' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const segmentLabels = this.entryData.metadata.allSegments.map(seg => ({ id: seg.id, label: seg.biological_annotation.name ? `<b>${seg.biological_annotation.name}</b>` : '' }));
const volumeNode = await this.entryData.newUpdate().to(group)
.apply(Download, { url, isBinary: true, label: `Segmentation Data: ${url}` })
.apply(ParseCif)
.apply(VolumeFromSegmentationCif, { blockHeader: 'SEGMENTATION_DATA', segmentLabels: segmentLabels, ownerId: this.entryData.ref })
.commit();
const volumeData = volumeNode.data as Volume;
const segmentation = Volume.Segmentation.get(volumeData);
const segmentIds: number[] = Array.from(segmentation?.segments.keys() ?? []);
await this.entryData.newUpdate().to(volumeNode)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.entryData.plugin, volumeData, {
type: 'segment',
typeParams: { tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
color: 'volume-segment',
colorParams: { palette: this.createPalette(segmentIds) },
}), { tags: [SEGMENT_VISUAL_TAG] }).commit();
}
}
private createPalette(segmentIds: number[]) {
const colorMap = new Map<number, Color>();
for (const segment of this.entryData.metadata.allSegments) {
const color = Color.fromNormalizedArray(segment.colour, 0);
colorMap.set(segment.id, color);
}
if (colorMap.size === 0) return undefined;
for (const segid of segmentIds) {
colorMap.get(segid);
}
const colors = segmentIds.map(segid => colorMap.get(segid) ?? DEFAULT_SEGMENT_COLOR);
return { name: 'colors' as const, params: { list: { kind: 'set' as const, colors: colors } } };
}
async updateOpacity(opacity: number) {
const reprs = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
const update = this.entryData.newUpdate();
for (const s of reprs) {
update.to(s).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.alpha = opacity; });
}
return await update.commit();
}
private makeLoci(segments: number[]) {
const vis = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
if (!vis) return undefined;
const repr = vis.obj?.data.repr;
const wholeLoci = repr.getAllLoci()[0];
if (!wholeLoci || !Volume.Segment.isLoci(wholeLoci)) return undefined;
return { loci: Volume.Segment.Loci(wholeLoci.volume, segments), repr: repr };
}
async highlightSegment(segment: Segment) {
const segmentLoci = this.makeLoci([segment.id]);
if (!segmentLoci) return;
this.entryData.plugin.managers.interactivity.lociHighlights.highlight(segmentLoci, false);
}
async selectSegment(segment?: number) {
if (segment === undefined || segment < 0) return;
const segmentLoci = this.makeLoci([segment]);
if (!segmentLoci) return;
this.entryData.plugin.managers.interactivity.lociSelects.select(segmentLoci, false);
}
/** Make visible the specified set of lattice segments */
async showSegments(segments: number[]) {
const repr = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG)[0];
if (!repr) return;
const selectedSegment = this.entryData.currentState.value.selectedSegment;
const mustReselect = segments.includes(selectedSegment) && !repr.params?.values.type.params.segments.includes(selectedSegment);
const update = this.entryData.newUpdate();
update.to(repr).update(StateTransforms.Representation.VolumeRepresentation3D, p => { p.type.params.segments = segments; });
await update.commit();
if (mustReselect) {
await this.selectSegment(this.entryData.currentState.value.selectedSegment);
}
}
async setTryUseGpu(tryUseGpu: boolean) {
const visuals = this.entryData.findNodesByTags(SEGMENT_VISUAL_TAG);
for (const visual of visuals) {
const oldParams: VolumeVisualParams = visual.transform.params;
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
}
}

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject } from '../../mol-plugin-state/objects';
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>
export const VolsegStateParams = {
volumeType: VolumeTypeChoice.PDSelect(),
volumeIsovalueKind: PD.Select('relative', [['relative', 'Relative'], ['absolute', 'Absolute']]),
volumeIsovalueValue: PD.Numeric(1),
volumeOpacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }),
segmentOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.05 }),
selectedSegment: PD.Numeric(-1, { step: 1 }),
visibleSegments: PD.ObjectList({ segmentId: PD.Numeric(0) }, s => s.segmentId.toString()),
visibleModels: PD.ObjectList({ pdbId: PD.Text('') }, s => s.pdbId.toString()),
};
export type VolsegStateData = PD.Values<typeof VolsegStateParams>;
export class VolsegState extends PluginStateObject.Create<VolsegStateData>({ name: 'Vol & Seg Entry State', typeClass: 'Data' }) { }
export const VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME = 'volseg-state-from-entry'; // defined here to avoid cyclic dependency

View File

@@ -0,0 +1,191 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Vec2 } from '../../mol-math/linear-algebra';
import { Volume } from '../../mol-model/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 { Download } from '../../mol-plugin-state/transforms/data';
import { CreateGroup } from '../../mol-plugin-state/transforms/misc';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { PluginCommands } from '../../mol-plugin/commands';
import { StateObjectSelector } from '../../mol-state';
import { Color } from '../../mol-util/color';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { BOX, VolsegEntryData, MAX_VOXELS } from './entry-root';
import { VolsegStateParams, VolumeTypeChoice } from './entry-state';
import * as ExternalAPIs from './external-api';
import { VolsegGlobalStateData } from './global-state';
const GROUP_TAG = 'volume-group';
export const VOLUME_VISUAL_TAG = 'volume-visual';
const DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH = 0.5;
export type VolumeVisualParams = ReturnType<typeof createVolumeRepresentationParams>;
interface VolumeStats { min: number, max: number, mean: number, sigma: number };
export const SimpleVolumeParams = {
volumeType: VolumeTypeChoice.PDSelect(),
opacity: PD.Numeric(0.2, { min: 0, max: 1, step: 0.05 }, { hideIf: p => p.volumeType === 'off' }),
};
export type SimpleVolumeParamValues = PD.Values<typeof SimpleVolumeParams>;
export class VolsegVolumeData {
private entryData: VolsegEntryData;
private visualTypeParamCache: { [type: string]: any } = {};
constructor(rootData: VolsegEntryData) {
this.entryData = rootData;
}
async loadVolume() {
const hasVolumes = this.entryData.metadata.raw.grid.volumes.volume_downsamplings.length > 0;
if (hasVolumes) {
const isoLevelPromise = ExternalAPIs.tryGetIsovalue(this.entryData.metadata.raw.grid.general.source_db_id ?? this.entryData.entryId);
let group = this.entryData.findNodesByTags(GROUP_TAG)[0]?.transform.ref;
if (!group) {
const newGroupNode = await this.entryData.newUpdate().apply(CreateGroup, { label: 'Volume' }, { tags: [GROUP_TAG], state: { isCollapsed: true } }).commit();
group = newGroupNode.ref;
}
const url = this.entryData.api.volumeUrl(this.entryData.source, this.entryData.entryId, BOX, MAX_VOXELS);
const data = await this.entryData.newUpdate().to(group).apply(Download, { url, isBinary: true, label: `Volume Data: ${url}` }).commit();
const parsed = await this.entryData.plugin.dataFormats.get('dscif')!.parse(this.entryData.plugin, data);
const volumeNode: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
const volumeData = volumeNode.cell!.obj!.data;
const volumeType = VolsegStateParams.volumeType.defaultValue;
let isovalue = await isoLevelPromise;
if (!isovalue) {
const stats = volumeData.grid.stats;
const maxRelative = (stats.max - stats.mean) / stats.sigma;
if (maxRelative > 1) {
isovalue = { kind: 'relative', value: 1.0 };
} else {
isovalue = { kind: 'relative', value: maxRelative * 0.5 };
}
}
const adjustedIsovalue = Volume.adjustedIsoValue(volumeData, isovalue.value, isovalue.kind);
const visualParams = this.createVolumeVisualParams(volumeData, volumeType);
this.changeIsovalueInVolumeVisualParams(visualParams, adjustedIsovalue, volumeData.grid.stats);
await this.entryData.newUpdate()
.to(volumeNode)
.apply(StateTransforms.Representation.VolumeRepresentation3D, visualParams, { tags: [VOLUME_VISUAL_TAG], state: { isHidden: volumeType === 'off' } })
.commit();
return { isovalue: adjustedIsovalue };
}
}
async setVolumeVisual(type: 'isosurface' | 'direct-volume' | 'off') {
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (!visual) return;
const oldParams: VolumeVisualParams = visual.transform.params;
this.visualTypeParamCache[oldParams.type.name] = oldParams.type.params;
if (type === 'off') {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
} else {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
if (oldParams.type.name === type) return;
const newParams: VolumeVisualParams = {
...oldParams,
type: {
name: type,
params: this.visualTypeParamCache[type] ?? oldParams.type.params,
}
};
const volumeStats = visual.obj?.data.sourceData.grid.stats;
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
this.changeIsovalueInVolumeVisualParams(newParams, undefined, volumeStats);
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async updateVolumeVisual(newParams: SimpleVolumeParamValues) {
const { volumeType, opacity } = newParams;
const visual = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG)[0];
if (!visual) return;
const oldVisualParams: VolumeVisualParams = visual.transform.params;
this.visualTypeParamCache[oldVisualParams.type.name] = oldVisualParams.type.params;
if (volumeType === 'off') {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, true); // true means hide, ¯\_(ツ)_/¯
} else {
setSubtreeVisibility(this.entryData.plugin.state.data, visual.transform.ref, false); // true means hide, ¯\_(ツ)_/¯
const newVisualParams: VolumeVisualParams = {
...oldVisualParams,
type: {
name: volumeType,
params: this.visualTypeParamCache[volumeType] ?? oldVisualParams.type.params,
}
};
newVisualParams.type.params.alpha = opacity;
const volumeStats = visual.obj?.data.sourceData.grid.stats;
if (!volumeStats) throw new Error(`Cannot get volume stats from volume visual ${visual.transform.ref}`);
this.changeIsovalueInVolumeVisualParams(newVisualParams, undefined, volumeStats);
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newVisualParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
async setTryUseGpu(tryUseGpu: boolean) {
const visuals = this.entryData.findNodesByTags(VOLUME_VISUAL_TAG);
for (const visual of visuals) {
const oldParams: VolumeVisualParams = visual.transform.params;
if (oldParams.type.params.tryUseGpu === !tryUseGpu) {
const newParams = { ...oldParams, type: { ...oldParams.type, params: { ...oldParams.type.params, tryUseGpu: tryUseGpu } } };
const update = this.entryData.newUpdate().to(visual.transform.ref).update(newParams);
await PluginCommands.State.Update(this.entryData.plugin, { state: this.entryData.plugin.state.data, tree: update, options: { doNotUpdateCurrent: true } });
}
}
}
private getIsovalueFromState(): Volume.IsoValue {
const { volumeIsovalueKind, volumeIsovalueValue } = this.entryData.currentState.value;
return volumeIsovalueKind === 'relative'
? Volume.IsoValue.relative(volumeIsovalueValue)
: Volume.IsoValue.absolute(volumeIsovalueValue);
}
private createVolumeVisualParams(volume: Volume, type: 'isosurface' | 'direct-volume' | 'off'): VolumeVisualParams {
if (type === 'off') type = 'isosurface';
return createVolumeRepresentationParams(this.entryData.plugin, volume, {
type: type,
typeParams: { alpha: 0.2, tryUseGpu: VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu },
color: 'uniform',
colorParams: { value: Color(0x121212) },
});
}
private changeIsovalueInVolumeVisualParams(params: VolumeVisualParams, isovalue: Volume.IsoValue | undefined, stats: VolumeStats) {
isovalue ??= this.getIsovalueFromState();
switch (params.type.name) {
case 'isosurface':
params.type.params.isoValue = isovalue;
params.type.params.tryUseGpu = VolsegGlobalStateData.getGlobalState(this.entryData.plugin)?.tryUseGpu;
break;
case 'direct-volume':
const absIso = Volume.IsoValue.toAbsolute(isovalue, stats).absoluteValue;
const fractIso = (absIso - stats.min) / (stats.max - stats.min);
const peakHalfwidth = DIRECT_VOLUME_RELATIVE_PEAK_HALFWIDTH * stats.sigma / (stats.max - stats.min);
params.type.params.controlPoints = [
Vec2.create(Math.max(fractIso - peakHalfwidth, 0), 0),
Vec2.create(fractIso, 1),
Vec2.create(Math.min(fractIso + peakHalfwidth, 1), 0),
];
break;
}
}
}

View File

@@ -0,0 +1,51 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { splitEntryId } from './helpers';
/** Try to get author-defined contour value for isosurface from EMDB API. Return relative value 1.0, if not applicable or fails. */
export async function tryGetIsovalue(entryId: string): Promise<{ kind: 'absolute' | 'relative', value: number } | undefined> {
const split = splitEntryId(entryId);
if (split.source === 'emdb') {
try {
const response = await fetch(`https://www.ebi.ac.uk/emdb/api/entry/map/${split.entryNumber}`);
const json = await response.json();
const contours: any[] = json?.map?.contour_list?.contour;
if (contours && contours.length > 0) {
const theContour = contours.find(c => c.primary) || contours[0];
if (theContour.level === undefined) throw new Error('EMDB API response missing contour level.');
return { kind: 'absolute', value: theContour.level };
}
} catch {
// do nothing
}
}
return undefined;
}
export async function getPdbIdsForEmdbEntry(entryId: string): Promise<string[]> {
const split = splitEntryId(entryId);
const result = [];
if (split.source === 'emdb') {
entryId = entryId.toUpperCase();
const apiUrl = `https://www.ebi.ac.uk/pdbe/api/emdb/entry/fitted/${entryId}`;
try {
const response = await fetch(apiUrl);
if (response.ok) {
const json = await response.json();
const jsonEntry = json[entryId] ?? [];
for (const record of jsonEntry) {
const pdbs = record?.fitted_emdb_id_list?.pdb_id ?? [];
result.push(...pdbs);
}
}
} catch (ex) {
// do nothing
}
}
return result;
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { BehaviorSubject } from 'rxjs';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginContext } from '../../mol-plugin/context';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { VolsegEntry } from './entry-root';
import { isDefined } from './helpers';
export const VolsegGlobalStateParams = {
tryUseGpu: PD.Boolean(true, { description: 'Attempt using GPU for faster rendering. \nCaution: with some hardware setups, this might render some objects incorrectly or not at all.' }),
selectionMode: PD.Boolean(true, { description: 'Allow selecting/deselecting a segment by clicking on it.' }),
};
export type VolsegGlobalStateParamValues = PD.Values<typeof VolsegGlobalStateParams>;
export class VolsegGlobalState extends PluginStateObject.CreateBehavior<VolsegGlobalStateData>({ name: 'Vol & Seg Global State' }) { }
export class VolsegGlobalStateData extends PluginBehavior.WithSubscribers<VolsegGlobalStateParamValues> {
private ref: string;
currentState = new BehaviorSubject(PD.getDefaultValues(VolsegGlobalStateParams));
constructor(plugin: PluginContext, params: VolsegGlobalStateParamValues) {
super(plugin, params);
this.currentState.next(params);
}
register(ref: string) {
this.ref = ref;
}
unregister() {
this.ref = '';
}
isRegistered() {
return this.ref !== '';
}
async updateState(plugin: PluginContext, state: Partial<VolsegGlobalStateParamValues>) {
const oldState = this.currentState.value;
const promises = [];
const allEntries = plugin.state.data.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell.obj?.data).filter(isDefined);
if (state.tryUseGpu !== undefined && state.tryUseGpu !== oldState.tryUseGpu) {
for (const entry of allEntries) {
promises.push(entry.setTryUseGpu(state.tryUseGpu));
}
}
if (state.selectionMode !== undefined && state.selectionMode !== oldState.selectionMode) {
for (const entry of allEntries) {
promises.push(entry.setSelectionMode(state.selectionMode));
}
}
await Promise.all(promises);
await plugin.build().to(this.ref).update(state).commit();
}
static getGlobalState(plugin: PluginContext): VolsegGlobalStateParamValues | undefined {
return plugin.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data.currentState.value;
}
}

View File

@@ -0,0 +1,163 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Volume } from '../../mol-model/volume';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { setSubtreeVisibility } from '../../mol-plugin/behavior/static/state';
import { StateBuilder, StateObjectSelector, StateTransformer } from '../../mol-state';
import { ParamDefinition } from '../../mol-util/param-definition';
import { Source } from './entry-root';
/** Split entry ID (e.g. 'emd-1832') into source ('emdb') and number ('1832') */
export function splitEntryId(entryId: string) {
const PREFIX_TO_SOURCE: { [prefix: string]: Source } = { 'emd': 'emdb' };
const [prefix, entry] = entryId.split('-');
return {
source: PREFIX_TO_SOURCE[prefix] ?? prefix,
entryNumber: entry
};
}
/** Create entry ID (e.g. 'emd-1832') for a combination of source ('emdb') and number ('1832') */
export function createEntryId(source: Source, entryNumber: string | number) {
const SOURCE_TO_PREFIX: { [prefix: string]: string } = { 'emdb': 'emd' };
const prefix = SOURCE_TO_PREFIX[source] ?? source;
return `${prefix}-${entryNumber}`;
}
/**
* Represents a set of values to choose from, with a default value. Example:
* ```
* export const MyChoice = new Choice({ yes: 'I agree', no: 'Nope' }, 'yes');
* export type MyChoiceType = Choice.Values<typeof MyChoice>; // 'yes'|'no'
* ```
*/
export class Choice<T extends string, D extends T> {
readonly defaultValue: D;
readonly options: [T, string][];
private readonly nameDict: { [value in T]: string };
constructor(opts: { [value in T]: string }, defaultValue: D) {
this.defaultValue = defaultValue;
this.options = Object.keys(opts).map(k => [k as T, opts[k as T]]);
this.nameDict = opts;
}
PDSelect(defaultValue?: T, info?: ParamDefinition.Info): ParamDefinition.Select<T> {
return ParamDefinition.Select<T>(defaultValue ?? this.defaultValue, this.options, info);
}
prettyName(value: T): string {
return this.nameDict[value];
}
get values(): T[] {
return this.options.map(([value, pretty]) => value);
}
}
export namespace Choice {
export type Values<T extends Choice<any, any>> = T extends Choice<infer R, any> ? R : any;
}
export function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
export class NodeManager {
private nodes: { [key: string]: StateObjectSelector };
constructor() {
this.nodes = {};
}
private static nodeExists(node: StateObjectSelector): boolean {
try {
return node.checkValid();
} catch {
return false;
}
}
public getNode(key: string): StateObjectSelector | undefined {
const node = this.nodes[key];
if (node && !NodeManager.nodeExists(node)) {
delete this.nodes[key];
return undefined;
}
return node;
}
public getNodes(): StateObjectSelector[] {
return Object.keys(this.nodes).map(key => this.getNode(key)).filter(node => node) as StateObjectSelector[];
}
public deleteAllNodes(update: StateBuilder.Root) {
for (const node of this.getNodes()) {
update.delete(node);
}
this.nodes = {};
}
public hideAllNodes() {
for (const node of this.getNodes()) {
setSubtreeVisibility(node.state!, node.ref, true); // hide
}
}
public async showNode(key: string, factory: () => StateObjectSelector | Promise<StateObjectSelector>, forceVisible: boolean = true) {
let node = this.getNode(key);
if (node) {
if (forceVisible) {
setSubtreeVisibility(node.state!, node.ref, false); // show
}
} else {
node = await factory();
this.nodes[key] = node;
}
return node;
}
}
const CreateTransformer = StateTransformer.builderFactory('volseg');
export const CreateVolume = CreateTransformer({
name: 'create-transformer',
from: PluginStateObject.Root,
to: PluginStateObject.Volume.Data,
params: {
label: ParamDefinition.Text('Volume', { isHidden: true }),
description: ParamDefinition.Text('', { isHidden: true }),
volume: ParamDefinition.Value<Volume>(undefined as any, { isHidden: true }),
}
})({
apply({ params }) {
return new PluginStateObject.Volume.Data(params.volume, { label: params.label, description: params.description });
}
});
export function applyEllipsis(name: string, max_chars: number = 60) {
if (name.length <= max_chars) return name;
const beginning = name.substring(0, max_chars);
let lastSpace = beginning.lastIndexOf(' ');
if (lastSpace === -1) return beginning + '...';
if (lastSpace > 0 && ',;.'.includes(name.charAt(lastSpace - 1))) lastSpace--;
return name.substring(0, lastSpace) + '...';
}
export function lazyGetter<T>(getter: () => T, errorIfUndefined?: string) {
let value: T | undefined = undefined;
return () => {
if (value === undefined) value = getter();
if (errorIfUndefined && value === undefined) throw new Error(errorIfUndefined);
return value;
};
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject as SO } from '../../mol-plugin-state/objects';
import { PluginBehavior } from '../../mol-plugin/behavior';
import { PluginConfigItem } from '../../mol-plugin/config';
import { PluginContext } from '../../mol-plugin/context';
import { StateAction } from '../../mol-state';
import { Task } from '../../mol-task';
import { DEFAULT_VOLSEG_SERVER, VolumeApiV2 } from './volseg-api/api';
import { VolsegEntryData, VolsegEntryParamValues, createLoadVolsegParams } from './entry-root';
import { VolsegGlobalState } from './global-state';
import { createEntryId } from './helpers';
import { VolsegEntryFromRoot, VolsegGlobalStateFromRoot, VolsegStateFromEntry } from './transformers';
import { VolsegUI } from './ui';
const DEBUGGING = typeof window !== 'undefined' ? window?.location?.hostname === 'localhost' : false;
export const VolsegVolumeServerConfig = {
// DefaultServer: new PluginConfigItem('volseg-volume-server', DEFAULT_VOLUME_SERVER_V2),
DefaultServer: new PluginConfigItem('volseg-volume-server', DEBUGGING ? 'http://localhost:9000/v2' : DEFAULT_VOLSEG_SERVER),
};
export const Volseg = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
name: 'volseg',
category: 'misc',
display: {
name: 'Volseg',
description: 'Volseg'
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showTooltip: boolean }> {
register() {
this.ctx.state.data.actions.add(LoadVolseg);
this.ctx.customStructureControls.set('volseg', VolsegUI as any);
this.initializeEntryLists(); // do not await
const entries = new Map<string, VolsegEntryData>();
this.subscribeObservable(this.ctx.state.data.events.cell.created, o => {
if (o.cell.obj instanceof VolsegEntryData) entries.set(o.ref, o.cell.obj);
});
this.subscribeObservable(this.ctx.state.data.events.cell.removed, o => {
if (entries.has(o.ref)) {
entries.get(o.ref)!.dispose();
entries.delete(o.ref);
}
});
}
unregister() {
this.ctx.state.data.actions.remove(LoadVolseg);
this.ctx.customStructureControls.delete('volseg');
}
private async initializeEntryLists() {
const apiUrl = this.ctx.config.get(VolsegVolumeServerConfig.DefaultServer) ?? DEFAULT_VOLSEG_SERVER;
const api = new VolumeApiV2(apiUrl);
const entryLists = await api.getEntryList(10 ** 6);
Object.values(entryLists).forEach(l => l.sort());
(this.ctx.customState as any).volsegAvailableEntries = entryLists;
}
}
});
export const LoadVolseg = StateAction.build({
display: { name: 'Load Volume & Segmentation' },
from: SO.Root,
params: (a, plugin: PluginContext) => {
const res = createLoadVolsegParams(plugin, (plugin.customState as any).volsegAvailableEntries);
return res;
},
})(({ params, state }, ctx: PluginContext) => Task.create('Loading Volume & Segmentation', taskCtx => {
return state.transaction(async () => {
const entryParams = VolsegEntryParamValues.fromLoadVolsegParamValues(params);
if (entryParams.entryId.trim().length === 0) {
alert('Must specify Entry Id!');
throw new Error('Specify Entry Id');
}
if (!entryParams.entryId.includes('-')) {
// add source prefix if the user omitted it (e.g. 1832 -> emd-1832)
entryParams.entryId = createEntryId(entryParams.source, entryParams.entryId);
}
ctx.behaviors.layout.leftPanelTabName.next('data');
const globalStateNode = ctx.state.data.selectQ(q => q.ofType(VolsegGlobalState))[0];
if (!globalStateNode) {
await state.build().toRoot().apply(VolsegGlobalStateFromRoot, {}, { state: { isGhost: !DEBUGGING } }).commit();
}
const entryNode = await state.build().toRoot().apply(VolsegEntryFromRoot, entryParams).commit();
await state.build().to(entryNode).apply(VolsegStateFromEntry, {}, { state: { isGhost: !DEBUGGING } }).commit();
if (entryNode.data) {
await entryNode.data.loadVolume();
await entryNode.data.loadSegmentations();
}
}).runInContext(taskCtx);
}));

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { PluginStateObject, PluginStateTransform } from '../../mol-plugin-state/objects';
import { PluginContext } from '../../mol-plugin/context';
import { StateTransformer } from '../../mol-state';
import { Task } from '../../mol-task';
import { VolsegEntry, VolsegEntryData, createVolsegEntryParams } from './entry-root';
import { VolsegState, VolsegStateParams, VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME } from './entry-state';
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
export const VolsegEntryFromRoot = PluginStateTransform.BuiltIn({
name: 'volseg-entry-from-root',
display: { name: 'Vol & Seg Entry', description: 'Vol & Seg Entry' },
from: PluginStateObject.Root,
to: VolsegEntry,
params: (a, plugin: PluginContext) => createVolsegEntryParams(plugin),
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Load Vol & Seg Entry', async () => {
const data = await VolsegEntryData.create(plugin, params);
return new VolsegEntry(data, { label: data.entryId, description: 'Vol & Seg Entry' });
});
},
update({ b, oldParams, newParams }) {
Object.assign(newParams, oldParams);
console.error('Changing params of existing VolsegEntry node is not allowed');
return StateTransformer.UpdateResult.Unchanged;
}
});
export const VolsegStateFromEntry = PluginStateTransform.BuiltIn({
name: VOLSEG_STATE_FROM_ENTRY_TRANSFORMER_NAME,
display: { name: 'Vol & Seg Entry State', description: 'Vol & Seg Entry State' },
from: VolsegEntry,
to: VolsegState,
params: VolsegStateParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Create Vol & Seg Entry State', async () => {
return new VolsegState(params, { label: 'State' });
});
}
});
export const VolsegGlobalStateFromRoot = PluginStateTransform.BuiltIn({
name: 'volseg-global-state-from-root',
display: { name: 'Vol & Seg Global State', description: 'Vol & Seg Global State' },
from: PluginStateObject.Root,
to: VolsegGlobalState,
params: VolsegGlobalStateParams,
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Create Vol & Seg Global State', async () => {
const data = new VolsegGlobalStateData(plugin, params);
return new VolsegGlobalState(data, { label: 'Global State', description: 'Vol & Seg Global State' });
});
},
update({ b, oldParams, newParams }) {
b.data.currentState.next(newParams);
return StateTransformer.UpdateResult.Updated;
}
});

View File

@@ -0,0 +1,264 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { CollapsableControls, CollapsableState } from '../../mol-plugin-ui/base';
import { Button, ControlRow, ExpandGroup, IconButton } from '../../mol-plugin-ui/controls/common';
import * as Icons from '../../mol-plugin-ui/controls/icons';
import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
import { Slider } from '../../mol-plugin-ui/controls/slider';
import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
import { UpdateTransformControl } from '../../mol-plugin-ui/state/update-transform';
import { PluginContext } from '../../mol-plugin/context';
import { shallowEqualArrays } from '../../mol-util';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { sleep } from '../../mol-util/sleep';
import { VolsegEntry, VolsegEntryData } from './entry-root';
import { SimpleVolumeParams, SimpleVolumeParamValues } from './entry-volume';
import { VolsegGlobalState, VolsegGlobalStateData, VolsegGlobalStateParams } from './global-state';
import { isDefined } from './helpers';
interface VolsegUIData {
globalState?: VolsegGlobalStateData,
availableNodes: VolsegEntry[],
activeNode?: VolsegEntry,
}
namespace VolsegUIData {
export function changeAvailableNodes(data: VolsegUIData, newNodes: VolsegEntry[]): VolsegUIData {
const newActiveNode = newNodes.length > data.availableNodes.length ?
newNodes[newNodes.length - 1]
: newNodes.find(node => node.data.ref === data.activeNode?.data.ref) ?? newNodes[0];
return { ...data, availableNodes: newNodes, activeNode: newActiveNode };
}
export function changeActiveNode(data: VolsegUIData, newActiveRef: string): VolsegUIData {
const newActiveNode = data.availableNodes.find(node => node.data.ref === newActiveRef) ?? data.availableNodes[0];
return { ...data, availableNodes: data.availableNodes, activeNode: newActiveNode };
}
export function equals(data1: VolsegUIData, data2: VolsegUIData) {
return shallowEqualArrays(data1.availableNodes, data2.availableNodes) && data1.activeNode === data2.activeNode && data1.globalState === data2.globalState;
}
}
export class VolsegUI extends CollapsableControls<{}, { data: VolsegUIData }> {
protected defaultState(): CollapsableState & { data: VolsegUIData } {
return {
header: 'Volume & Segmentation',
isCollapsed: true,
brand: { accent: 'orange', svg: Icons.ExtensionSvg },
data: {
globalState: undefined,
availableNodes: [],
activeNode: undefined,
}
};
}
protected renderControls(): JSX.Element | null {
return <VolsegControls plugin={this.plugin} data={this.state.data} setData={d => this.setState({ data: d })} />;
}
componentDidMount(): void {
this.setState({ isHidden: true, isCollapsed: false });
this.subscribe(this.plugin.state.data.events.changed, e => {
const nodes = e.state.selectQ(q => q.ofType(VolsegEntry)).map(cell => cell?.obj).filter(isDefined);
const isHidden = nodes.length === 0;
const newData = VolsegUIData.changeAvailableNodes(this.state.data, nodes);
if (!this.state.data.globalState?.isRegistered()) {
const globalState = e.state.selectQ(q => q.ofType(VolsegGlobalState))[0]?.obj?.data;
if (globalState) newData.globalState = globalState;
}
if (!VolsegUIData.equals(this.state.data, newData) || this.state.isHidden !== isHidden) {
this.setState({ data: newData, isHidden: isHidden });
}
});
}
}
function VolsegControls({ plugin, data, setData }: { plugin: PluginContext, data: VolsegUIData, setData: (d: VolsegUIData) => void }) {
const entryData = data.activeNode?.data;
if (!entryData) {
return <p>No data!</p>;
}
if (!data.globalState) {
return <p>No global state!</p>;
}
const params = {
/** Reference to the active VolsegEntry node */
entry: PD.Select(data.activeNode!.data.ref, data.availableNodes.map(entry => [entry.data.ref, entry.data.entryId]))
};
const values: PD.Values<typeof params> = {
entry: data.activeNode!.data.ref,
};
const globalState = useBehavior(data.globalState.currentState);
return <>
<ParameterControls params={params} values={values} onChangeValues={next => setData(VolsegUIData.changeActiveNode(data, next.entry))} />
<ExpandGroup header='Global options'>
<WaitingParameterControls params={VolsegGlobalStateParams} values={globalState} onChangeValues={async next => await data.globalState?.updateState(plugin, next)} />
</ExpandGroup>
<VolsegEntryControls entryData={entryData} key={entryData.ref} />
</>;
}
function VolsegEntryControls({ entryData }: { entryData: VolsegEntryData }) {
const state = useBehavior(entryData.currentState);
const allSegments = entryData.metadata.allSegments;
const selectedSegment = entryData.metadata.getSegment(state.selectedSegment);
const visibleSegments = state.visibleSegments.map(seg => seg.segmentId);
const visibleModels = state.visibleModels.map(model => model.pdbId);
const allPdbs = entryData.pdbs;
return <>
{/* Title */}
<div style={{ fontWeight: 'bold', padding: 8, paddingTop: 6, paddingBottom: 4, overflow: 'hidden' }}>
{entryData.metadata.raw.annotation?.name ?? 'Unnamed Annotation'}
</div>
{/* Fitted models */}
{allPdbs.length > 0 && <ExpandGroup header='Fitted models in PDB' initiallyExpanded>
{allPdbs.map(pdb =>
<WaitingButton key={pdb} onClick={() => entryData.actionShowFittedModel(visibleModels.includes(pdb) ? [] : [pdb])}
style={{ fontWeight: visibleModels.includes(pdb) ? 'bold' : undefined, textAlign: 'left', marginTop: 1 }}>
{pdb}
</WaitingButton>
)}
</ExpandGroup>}
{/* Volume */}
<VolumeControls entryData={entryData} />
<ExpandGroup header='Segmentation data' initiallyExpanded>
{/* Segment opacity slider */}
<ControlRow label='Opacity' control={
<WaitingSlider min={0} max={1} value={state.segmentOpacity} step={0.05} onChange={async v => await entryData.actionSetOpacity(v)} />
} />
{/* Segment toggles */}
{allSegments.length > 0 && <>
<WaitingButton onClick={async () => { await sleep(20); await entryData.actionToggleAllSegments(); }} style={{ marginTop: 1 }}>
Toggle All segments
</WaitingButton>
<div style={{ maxHeight: 200, overflow: 'hidden', overflowY: 'auto', marginBlock: 1 }}>
{allSegments.map(segment =>
<div style={{ display: 'flex', marginBottom: 1 }} key={segment.id}
onMouseEnter={() => entryData.actionHighlightSegment(segment)}
onMouseLeave={() => entryData.actionHighlightSegment()}>
<Button onClick={() => entryData.actionSelectSegment(segment !== selectedSegment ? segment.id : undefined)}
style={{ fontWeight: segment.id === selectedSegment?.id ? 'bold' : undefined, marginRight: 1, flexGrow: 1, textAlign: 'left' }}>
<div title={segment.biological_annotation.name ?? 'Unnamed segment'} style={{ maxWidth: 240, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{segment.biological_annotation.name ?? 'Unnamed segment'} ({segment.id})
</div>
</Button>
<IconButton svg={visibleSegments.includes(segment.id) ? Icons.VisibilityOutlinedSvg : Icons.VisibilityOffOutlinedSvg}
onClick={() => entryData.actionToggleSegment(segment.id)} />
</div>
)}
</div>
</>}
</ExpandGroup>
{/* Segment annotations */}
<ExpandGroup header='Selected segment annotation' initiallyExpanded>
<div style={{ paddingTop: 4, paddingRight: 8, maxHeight: 300, overflow: 'hidden', overflowY: 'auto' }}>
{!selectedSegment && 'No segment selected'}
{selectedSegment && <b>Segment {selectedSegment.id}:<br />{selectedSegment.biological_annotation.name ?? 'Unnamed segment'}</b>}
{selectedSegment?.biological_annotation.external_references.map(ref =>
<p key={ref.id} style={{ marginTop: 4 }}>
<small>{ref.resource}:{ref.accession}</small><br />
<b>{capitalize(ref.label)}</b><br />
{ref.description}
</p>)}
</div>
</ExpandGroup>
</>;
}
function VolumeControls({ entryData }: { entryData: VolsegEntryData }) {
const vol = useBehavior(entryData.currentVolume);
if (!vol) return null;
const volumeValues: SimpleVolumeParamValues = {
volumeType: vol.state.isHidden ? 'off' : vol.params?.type.name as any,
opacity: vol.params?.type.params.alpha,
};
return <ExpandGroup header='Volume data' initiallyExpanded>
<WaitingParameterControls params={SimpleVolumeParams} values={volumeValues} onChangeValues={async next => { await sleep(20); await entryData.actionUpdateVolumeVisual(next); }} />
<ExpandGroup header='Detailed Volume Params' headerStyle={{ marginTop: 1 }}>
<UpdateTransformControl state={entryData.plugin.state.data} transform={vol} customHeader='none' />
</ExpandGroup>
</ExpandGroup>;
}
type ComponentParams<T extends React.Component<any, any, any> | ((props: any) => JSX.Element)> =
T extends React.Component<infer P, any, any> ? P : T extends (props: infer P) => JSX.Element ? P : never;
function WaitingSlider({ value, onChange, ...etc }: { value: number, onChange: (value: number) => any } & ComponentParams<Slider>) {
const [changing, sliderValue, execute] = useAsyncChange(value);
return <Slider value={sliderValue} disabled={changing} onChange={newValue => execute(onChange, newValue)} {...etc} />;
}
function WaitingButton({ onClick, ...etc }: { onClick: () => any } & ComponentParams<typeof Button>) {
const [changing, _, execute] = useAsyncChange(undefined);
return <Button disabled={changing} onClick={() => execute(onClick, undefined)} {...etc}>
{etc.children}
</Button>;
}
function WaitingParameterControls<T extends PD.Params>({ values, onChangeValues, ...etc }: { values: PD.ValuesFor<T>, onChangeValues: (values: PD.ValuesFor<T>) => any } & ComponentParams<ParameterControls<T>>) {
const [changing, currentValues, execute] = useAsyncChange(values);
return <ParameterControls isDisabled={changing} values={currentValues} onChangeValues={newValue => execute(onChangeValues, newValue)} {...etc} />;
}
function capitalize(text: string) {
const first = text.charAt(0);
const rest = text.slice(1);
return first.toUpperCase() + rest;
}
function useAsyncChange<T>(initialValue: T) {
const [isExecuting, setIsExecuting] = useState(false);
const [value, setValue] = useState(initialValue);
const isMounted = useRef(false);
useEffect(() => setValue(initialValue), [initialValue]);
useEffect(() => {
isMounted.current = true;
return () => { isMounted.current = false; };
}, []);
const execute = useCallback(
async (func: (val: T) => Promise<any>, val: T) => {
setIsExecuting(true);
setValue(val);
try {
await func(val);
} catch (err) {
if (isMounted.current) {
setValue(initialValue);
}
throw err;
} finally {
if (isMounted.current) {
setIsExecuting(false);
}
}
},
[]
);
return [isExecuting, value, execute] as const;
}

View File

@@ -0,0 +1,65 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { type Metadata } from './data';
export const DEFAULT_VOLSEG_SERVER = 'https://molstarvolseg.ncbr.muni.cz/v2';
export class VolumeApiV2 {
public volumeServerUrl: string;
public constructor(volumeServerUrl: string = DEFAULT_VOLSEG_SERVER) {
this.volumeServerUrl = volumeServerUrl.replace(/\/$/, ''); // trim trailing slash
}
public entryListUrl(maxEntries: number, keyword?: string): string {
return `${this.volumeServerUrl}/list_entries/${maxEntries}/${keyword ?? ''}`;
}
public metadataUrl(source: string, entryId: string): string {
return `${this.volumeServerUrl}/${source}/${entryId}/metadata`;
}
public volumeUrl(source: string, entryId: string, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
if (box) {
const [[a1, a2, a3], [b1, b2, b3]] = box;
return `${this.volumeServerUrl}/${source}/${entryId}/volume/box/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
} else {
return `${this.volumeServerUrl}/${source}/${entryId}/volume/cell?max_points=${maxPoints}`;
}
}
public latticeUrl(source: string, entryId: string, segmentation: number, box: [[number, number, number], [number, number, number]] | null, maxPoints: number): string {
if (box) {
const [[a1, a2, a3], [b1, b2, b3]] = box;
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/box/${segmentation}/${a1}/${a2}/${a3}/${b1}/${b2}/${b3}?max_points=${maxPoints}`;
} else {
return `${this.volumeServerUrl}/${source}/${entryId}/segmentation/cell/${segmentation}?max_points=${maxPoints}`;
}
}
public meshUrl_Json(source: string, entryId: string, segment: number, detailLevel: number): string {
return `${this.volumeServerUrl}/${source}/${entryId}/mesh/${segment}/${detailLevel}`;
}
public meshUrl_Bcif(source: string, entryId: string, segment: number, detailLevel: number): string {
return `${this.volumeServerUrl}/${source}/${entryId}/mesh_bcif/${segment}/${detailLevel}`;
}
public volumeInfoUrl(source: string, entryId: string): string {
return `${this.volumeServerUrl}/${source}/${entryId}/volume_info`;
}
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
const response = await fetch(this.entryListUrl(maxEntries, keyword));
return await response.json();
}
public async getMetadata(source: string, entryId: string): Promise<Metadata> {
const url = this.metadataUrl(source, entryId);
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch metadata from ${url}`);
return await response.json();
}
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
export interface Metadata {
grid: {
general: {
details: string,
source_db_name: string,
source_db_id: string,
},
volumes: Volumes,
segmentation_lattices: SegmentationLattices,
segmentation_meshes: SegmentationMeshes,
},
annotation: Annotation | null,
}
export interface Volumes {
volume_downsamplings: number[],
voxel_size: { [downsampling: number]: Vector3 },
origin: Vector3,
grid_dimensions: Vector3,
sampled_grid_dimensions: { [downsampling: number]: Vector3 },
mean: { [downsampling: number]: number },
std: { [downsampling: number]: number },
min: { [downsampling: number]: number },
max: { [downsampling: number]: number },
volume_force_dtype: string,
}
export interface SegmentationLattices {
segmentation_lattice_ids: number[],
segmentation_downsamplings: { [lattice: number]: number[] },
}
export interface SegmentationMeshes {
mesh_component_numbers: {
segment_ids?: {
[segId: number]: {
detail_lvls: {
[detail: number]: {
mesh_ids: {
[meshId: number]: {
num_triangles: number,
num_vertices: number
}
}
}
}
}
}
}
detail_lvl_to_fraction: {
[lvl: number]: number
}
}
export interface Annotation {
name: string,
details: string,
segment_list: Segment[],
}
export interface Segment {
id: number,
colour: number[],
biological_annotation: BiologicalAnnotation,
}
export interface BiologicalAnnotation {
name: string,
external_references: ExternalReference[]
}
export interface ExternalReference {
id: number, resource: string, accession: string, label: string,
description: string
}
type Vector3 = [number, number, number];

View File

@@ -0,0 +1,74 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { Color } from '../../../mol-util/color';
import { Metadata, Segment } from './data';
export class MetadataWrapper {
raw: Metadata;
private segmentMap?: { [id: number]: Segment };
constructor(rawMetadata: Metadata) {
this.raw = rawMetadata;
}
get allSegments() {
return this.raw.annotation?.segment_list ?? [];
}
get allSegmentIds() {
return this.allSegments.map(segment => segment.id);
}
getSegment(segmentId: number): Segment | undefined {
if (!this.segmentMap) {
this.segmentMap = {};
for (const segment of this.allSegments) {
this.segmentMap[segment.id] = segment;
}
}
return this.segmentMap[segmentId];
}
getSegmentColor(segmentId: number): Color | undefined {
const colorArray = this.getSegment(segmentId)?.colour;
return colorArray ? Color.fromNormalizedArray(colorArray, 0) : undefined;
}
/** Get the list of detail levels available for the given mesh segment. */
getMeshDetailLevels(segmentId: number): number[] {
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
if (!segmentIds) return [];
const details = segmentIds[segmentId].detail_lvls;
return Object.keys(details).map(s => parseInt(s));
}
/** Get the worst available detail level that is not worse than preferredDetail.
* If preferredDetail is null, get the worst detail level overall.
* (worse = greater number) */
getSufficientMeshDetail(segmentId: number, preferredDetail: number | null) {
let availDetails = this.getMeshDetailLevels(segmentId);
if (preferredDetail !== null) {
availDetails = availDetails.filter(det => det <= preferredDetail);
}
return Math.max(...availDetails);
}
/** IDs of all segments available as meshes */
get meshSegmentIds() {
const segmentIds = this.raw.grid.segmentation_meshes.mesh_component_numbers.segment_ids;
if (!segmentIds) return [];
return Object.keys(segmentIds).map(s => parseInt(s));
}
get gridTotalVolume() {
const [vx, vy, vz] = this.raw.grid.volumes.voxel_size[1];
const [gx, gy, gz] = this.raw.grid.volumes.grid_dimensions;
return vx * vy * vz * gx * gy * gz;
}
}

View File

@@ -0,0 +1,47 @@
# Chemical Component Dictionary Extension
The [Chemical Component Dictionary (CCD)](https://www.wwpdb.org/data/ccd) describes all small molecules and monomers found in PDB entries. The dictionary provides a plethora of additional information not present in wwPDB archive structures such as chemical descriptors (SMILES & InChI) and stereochemical assignments, information on bond order and more. Most notably, the CCD provides 2 sets of coordinates:
- `ideal`: idealized/minimized coordinates, obtained using Molecular Networks' Corina, and if there are issues, OpenEye's OMEGA
- `model`: coordinates extracted from an archive structure
## How to Load a Component from URL
1. "Download Structure" -- switch "Source" to "URL"
2. Enter URL of component, e.g. https://files.rcsb.org/ligands/view/HEM.cif, leave "Format" as is
3. Click "Apply"
This parses the corresponding component into 2 models (1st: `ideal` coordinates, 2nd: `model` coordinates) and applies the default representaiton to the 1st model. `model` coordinates are available as 2nd model. Click the canvas to re-focus if you don't see anything after switching models due to the coordinates being far away.
## How to Visualize Components
There's a dedicated representation preset that faciliates the comparison of `ideal` and `model` coordinates.
1. Load a component as described above
2. Switch structure preset to "Chemical Component" (button in the top-right, in the "Structure" panel)
This creates a dedicated component for `ideal` as well as `model` coordinates and represents them as ball-and-stick. Initially, only `ideal` coordinates are shown. After toggling the visibility of `model` coordinates, they appear superimposed with the `ideal` coordinates.
## Examples & Test Cases
Ligand | Description | Details
-- | -- | --
https://files.rcsb.org/ligands/view/HEM.cif | metal coordination |
https://files.rcsb.org/ligands/view/FE.cif | +3 oxidation state |
https://files.rcsb.org/ligands/view/FE2.cif | +2 oxidation state |
https://files.rcsb.org/ligands/view/RUC.cif | transition metal |
https://files.rcsb.org/ligands/view/SF4.cif | Fe-S cluster | doesn't align nicely
https://files.rcsb.org/ligands/view/TBR.cif | coords identical |
https://files.rcsb.org/ligands/view/OER.cif | coords identical |
https://files.rcsb.org/ligands/view/FEA.cif | charges |
https://files.rcsb.org/ligands/view/PR2.cif | orientation differs |
https://files.rcsb.org/ligands/view/03R.cif | some atoms missing |
https://files.rcsb.org/ligands/view/02U.cif | many atoms missing |
https://files.rcsb.org/ligands/view/HC0.cif | no ideal coords | unrelated: O and H atoms clashing
https://files.rcsb.org/ligands/view/Q6O.cif | no model coords |
https://files.rcsb.org/ligands/view/H0C.cif | big ligand |
https://files.rcsb.org/ligands/view/2NC.cif | dual representation as PRD and CC |
https://files.rcsb.org/birds/view/PRDCC_000001.cif | PRDCC |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/BB87Q.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/7ZTVU.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9QRZS.cif | extended CCD identifier |
https://raw.githubusercontent.com/wwPDB/extended-wwPDB-identifier-examples/main/CCD/9ABCD.cif | extended CCD identifier |
https://files.rcsb.org/ligands/view/UNK.cif | CCD special: unknown amino acid | unrelated: some model H are placed far away
https://files.rcsb.org/ligands/view/UNX.cif | CCD special: unknown atom/ion | no ideal coordinates
https://files.rcsb.org/ligands/view/UNL.cif | CCD special: unknown ligand | no coordinates whatsoever

View File

@@ -0,0 +1,33 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
*/
import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
import { ChemicalComponentPreset, ChemicalCompontentTrajectoryHierarchyPreset } from './representation';
export const wwPDBChemicalComponentDictionary = PluginBehavior.create<{ }>({
name: 'wwpdb-chemical-component-dictionary',
category: 'representation',
display: {
name: 'wwPDB Chemical Compontent Dictionary',
description: 'Custom representation for data loaded from the CCD.'
},
ctor: class extends PluginBehavior.Handler<{ }> {
register(): void {
this.ctx.builders.structure.hierarchy.registerPreset(ChemicalCompontentTrajectoryHierarchyPreset);
this.ctx.builders.structure.representation.registerPreset(ChemicalComponentPreset);
}
update() {
return false;
}
unregister() {
this.ctx.builders.structure.hierarchy.unregisterPreset(ChemicalCompontentTrajectoryHierarchyPreset);
this.ctx.builders.structure.representation.unregisterPreset(ChemicalComponentPreset);
}
},
params: () => ({ })
});

View File

@@ -0,0 +1,165 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { StateObjectRef, StateTransform } from '../../../mol-state';
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { StructureRepresentationPresetProvider, presetStaticComponent } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { PluginContext } from '../../../mol-plugin/context';
import { Mat4 } from '../../../mol-math/linear-algebra';
import { Structure } from '../../../mol-model/structure';
import { CCDFormat } from '../../../mol-model-formats/structure/mmcif';
import { MinimizeRmsd } from '../../../mol-math/linear-algebra/3d/minimize-rmsd';
import { SetUtils } from '../../../mol-util/set';
import { TrajectoryHierarchyPresetProvider } from '../../../mol-plugin-state/builder/structure/hierarchy-preset';
import { capitalize } from '../../../mol-util/string';
const CCDParams = (a: PluginStateObject.Molecule.Trajectory | undefined, plugin: PluginContext) => ({
representationPresetParams: PD.Optional(PD.Group(StructureRepresentationPresetProvider.CommonParams)),
showOriginalCoordinates: PD.Optional(PD.Boolean(true, { description: `Show original coordinates for 'model' and 'ideal' structure and do not align them.` })),
shownCoordinateType: PD.Select('ideal', PD.arrayToOptions(['ideal', 'model', 'both'] as const), { description: `What coordinate sets are visible.` }),
...TrajectoryHierarchyPresetProvider.CommonParams(a, plugin)
});
export const ChemicalCompontentTrajectoryHierarchyPreset = TrajectoryHierarchyPresetProvider({
id: 'preset-trajectory-ccd',
display: {
name: 'Chemical Component', group: 'Preset',
description: 'Shows molecules from the Chemical Component Dictionary.'
},
isApplicable: o => {
return CCDFormat.is(o.data.representative.sourceData);
},
params: CCDParams,
async apply(trajectory, params, plugin) {
const tr = StateObjectRef.resolveAndCheck(plugin.state.data, trajectory)?.obj?.data;
if (!tr) return {};
const builder = plugin.builders.structure;
const idealModel = await builder.createModel(trajectory, { modelIndex: 0 });
const idealModelProperties = await builder.insertModelProperties(idealModel, params.modelProperties, { isCollapsed: true });
const idealStructure = await builder.createStructure(idealModelProperties || idealModel, { name: 'model', params: {} });
const idealStructureProperties = await builder.insertStructureProperties(idealStructure, params.structureProperties);
const representationPreset = params.representationPreset || ChemicalComponentPreset.id;
const representationPresetParams = params.representationPresetParams || {};
if (representationPresetParams.ignoreHydrogens === undefined) representationPresetParams.ignoreHydrogens = true;
// degenerate case where either model or ideal coordinates are missing
if (tr.frameCount !== 2) {
const coordinateType = CCDFormat.CoordinateType.get(idealModel.obj!.data);
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, coordinateType });
return { models: [idealModel], structures: [idealStructure] };
}
const modelModel = await builder.createModel(trajectory, { modelIndex: 1 });
const modelModelProperties = await builder.insertModelProperties(modelModel, params.modelProperties, { isCollapsed: true });
const modelStructure = await builder.createStructure(modelModelProperties || modelModel, { name: 'model', params: {} });
const modelStructureProperties = await builder.insertStructureProperties(modelStructure, params.structureProperties);
// align ideal and model coordinates
if (!params.showOriginalCoordinates) {
const [a, b] = getPositionTables(idealStructure.obj!.data, modelStructure.obj!.data);
if (!a) {
plugin.log.warn(`Cannot align chemical components whose atom sets are disjoint.`);
} else {
const { bTransform, rmsd } = MinimizeRmsd.compute({ a, b });
await transform(plugin, modelStructure.cell!, bTransform);
plugin.log.info(`Superposed [model] and [ideal] with RMSD ${rmsd.toFixed(2)}.`);
}
}
await builder.representation.applyPreset(idealStructureProperties, representationPreset, { ...representationPresetParams, coordinateType: 'ideal', isHidden: params.shownCoordinateType === 'model' });
await builder.representation.applyPreset(modelStructureProperties, representationPreset, { ...representationPresetParams, coordinateType: 'model', isHidden: params.shownCoordinateType === 'ideal' });
return { models: [idealModel, modelModel], structures: [idealStructure, modelStructure] };
}
});
function getPositionTables(s1: Structure, s2: Structure) {
const m1 = getAtomIdSerialMap(s1);
const m2 = getAtomIdSerialMap(s2);
const intersecting = SetUtils.intersection(new Set(m1.keys()), new Set(m2.keys()));
const ret = [
MinimizeRmsd.Positions.empty(intersecting.size),
MinimizeRmsd.Positions.empty(intersecting.size)
];
let o = 0;
intersecting.forEach(k => {
ret[0].x[o] = s1.model.atomicConformation.x[m1.get(k)!];
ret[0].y[o] = s1.model.atomicConformation.y[m1.get(k)!];
ret[0].z[o] = s1.model.atomicConformation.z[m1.get(k)!];
ret[1].x[o] = s2.model.atomicConformation.x[m2.get(k)!];
ret[1].y[o] = s2.model.atomicConformation.y[m2.get(k)!];
ret[1].z[o] = s2.model.atomicConformation.z[m2.get(k)!];
o++;
});
return ret;
}
function getAtomIdSerialMap(structure: Structure) {
const map = new Map<string, number>();
const { label_atom_id } = structure.model.atomicHierarchy.atoms;
for (let i = 0, il = label_atom_id.rowCount; i < il; ++i) {
const id = label_atom_id.value(i);
if (!map.has(id)) map.set(id, map.size);
}
return map;
}
function transform(plugin: PluginContext, s: StateObjectRef<PluginStateObject.Molecule.Structure>, matrix: Mat4) {
const b = plugin.state.data.build().to(s)
.insert(StateTransforms.Model.TransformStructureConformation, { transform: { name: 'matrix', params: { data: matrix, transpose: false } } });
return plugin.runTask(plugin.state.data.updateTree(b));
}
export const ChemicalComponentPreset = StructureRepresentationPresetProvider({
id: 'preset-structure-representation-chemical-component',
display: {
name: 'Chemical Component', group: 'Miscellaneous',
description: `Show 'Ideal' and 'Model' coordinates of chemical components.`
},
isApplicable: o => {
return CCDFormat.is(o.data.model.sourceData);
},
params: () => ({
...StructureRepresentationPresetProvider.CommonParams,
coordinateType: PD.Select<CCDFormat.CoordinateType>('ideal', PD.arrayToOptions(['ideal', 'model'])),
isHidden: PD.Boolean(false)
}),
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
if (!structureCell) return {};
const { coordinateType, isHidden } = params;
const components = {
[coordinateType]: await presetStaticComponent(plugin, structureCell, 'all', { label: capitalize(coordinateType), tags: [coordinateType] })
};
const structure = structureCell.obj!.data;
const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
const representations = {
[coordinateType]: builder.buildRepresentation(update, components[coordinateType], { type: 'ball-and-stick', typeParams }, { initialState: { isHidden } }),
};
// sync UI state
if (components[coordinateType]?.cell?.state && isHidden) {
StateTransform.assignState(components[coordinateType]!.cell!.state, { isHidden });
}
await update.commit({ revertOnError: true });
await StructureRepresentationPresetProvider.updateFocusRepr(plugin, structure, params.theme?.focus?.name, params.theme?.focus?.params);
return { components, representations };
}
});

View File

@@ -0,0 +1,301 @@
/**
* 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 { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { Model } from '../../../mol-model/structure';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { StructureComponent } from '../../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../../mol-plugin-state/transforms/representation';
import { setSubtreeVisibility } from '../../../mol-plugin/behavior/static/state';
import { PluginContext } from '../../../mol-plugin/context';
import { MolScriptBuilder } from '../../../mol-script/language/builder';
import { ColorNames } from '../../../mol-util/color/names';
/** Amount by which to expand the camera radius when zooming to atoms involved in struct_conn (angstroms) */
const EXTRA_RADIUS = 4;
/** Tags for state tree nodes managed by this extension */
const TAGS = {
RESIDUE_SEL: 'structconn-focus-residue-sel',
ATOM_SEL: 'structconn-focus-atom-sel',
RESIDUE_REPR: 'structconn-focus-residue-repr',
RESIDUE_NCI_REPR: 'structconn-focus-residue-nci-repr',
ATOM_REPR: 'structconn-focus-atom-repr',
} as const;
type VisualParams = ReturnType<typeof StructureRepresentation3D.createDefaultParams>
/** Parameters for 3D representation of atoms involved in struct_conn (pink bubbles) */
const ATOMS_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.25, sizeAspectRatio: 0.73, adjustCylinderLength: true, xrayShaded: true, aromaticBonds: false, multipleBonds: 'off', dashCount: 1, dashCap: false } },
colorTheme: { name: 'uniform', params: { value: ColorNames.magenta } },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** Parameters for 3D representation of residues involved in struct_conn (normal ball-and-stick) */
const RESIDUES_VISUAL_PARAMS: VisualParams = {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.16 } },
colorTheme: { name: 'element-symbol', params: {} },
sizeTheme: { name: 'physical', params: {} },
} as const;
/** All public functions provided by the StructConn extension */
export const wwPDBStructConnExtensionFunctions = {
/** Return an object with all struct_conn records for a loaded structure.
* Applies to the first structure belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure overall if `entry` is `undefined`.
*/
getStructConns(plugin: PluginContext, entry: string | undefined): { [id: string]: StructConnRecord } {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (structure) return extractStructConns(structure.model);
else return {};
},
/** Create visuals for residues and atoms involved in a struct_conn with ID `structConnId`
* and zoom on them. If `keepExisting` is false (default), remove any such visuals created by previous calls to this function.
* Also hide all carbohydrate SNFG visuals within the structure (as they would occlude our residues of interest).
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async inspectStructConn(plugin: PluginContext, entry: string | undefined, structConnId: string, keepExisting: boolean = false): Promise<number> {
const structNode = selectStructureNode(plugin, entry);
const structure = structNode?.obj?.data;
if (!structure) {
console.error('Structure not found');
return 0;
}
const conns: { [id: string]: StructConnRecord } = structure.model._staticPropertyData['wwpdb-struct-conn-extension-data'] ??= extractStructConns(structure.model);
const conn = conns[structConnId];
if (!conn) {
console.error(`The structure does not contain struct_conn "${structConnId}"`);
return 0;
}
if (!keepExisting) {
await removeAllStructConnInspections(plugin, structNode);
}
const nSelectedAtoms = await addStructConnInspection(plugin, structNode, conn);
hideSnfgNodes(plugin, structNode);
return nSelectedAtoms;
},
/** Remove anything created by `inspectStructConn` within the structure and
* make visible any carbohydrate SNFG visuals that have been hidden by `inspectStructConn`.
*/
async clearStructConnInspections(plugin: PluginContext, entry: string | undefined) {
const structNode = selectStructureNode(plugin, entry);
if (!structNode) return;
await removeAllStructConnInspections(plugin, structNode);
unhideSnfgNodes(plugin, structNode);
},
};
type StructNode = Exclude<ReturnType<typeof selectStructureNode>, undefined>
/** Return the first structure node belonging to `entry` (e.g. '1tqn'),
* or to the first loaded structure node overall if `entry` is `undefined`.
* Includes only "root" structures, not structure components. */
function selectStructureNode(plugin: PluginContext, entry: string | undefined) {
const structNodes = plugin.state.data
.selectQ(q => q.rootsOfType(PluginStateObject.Molecule.Structure));
if (entry) {
const result = structNodes.find(node => node.obj && node.obj.data.model.entry.toLowerCase() === entry.toLowerCase());
if (!result) {
console.warn(`Structure with entry ID "${entry}" was not found. Available structures: ${structNodes.map(node => node.obj?.data.model.entry)}`);
}
return result;
} else {
if (structNodes.length > 1) {
console.warn(`Structure entry ID was not specified, but there is more than one loaded structure (${structNodes.map(node => node.obj?.data.model.entry)}). Taking the first structure.`);
}
if (structNodes.length === 0) {
console.warn(`There are no loaded structures.`);
}
return structNodes[0];
}
}
/** Represents one partner (i.e. atom) of a struct_conn */
interface StructConnPartner {
asymId: string,
seqId: number | undefined,
authSeqId: number | undefined,
insCode: string,
compId: string,
atomId: string,
/** Alternative location (use empty string if not given) */
altId: string,
}
/** Represents a struct_conn (interaction between two partners) */
export interface StructConnRecord {
id: string,
distance: number,
partner1: StructConnPartner,
partner2: StructConnPartner,
}
/** Return an object with all struct_conn records read from mmCIF.
* Return {} if the model comes from another format than mmCIF.
*/
function extractStructConns(model: Model): { [id: string]: StructConnRecord } {
if (!MmcifFormat.is(model.sourceData)) {
console.error('Cannot get struct_conn because source data are not mmCIF.');
return {};
}
const mmcifData = model.sourceData.data;
const {
id,
ptnr1_label_asym_id: asym1,
ptnr1_label_seq_id: seq1,
ptnr1_auth_seq_id: authSeq1,
pdbx_ptnr1_PDB_ins_code: authInsCode1,
ptnr1_label_comp_id: comp1,
ptnr1_label_atom_id: atom1,
pdbx_ptnr1_label_alt_id: alt1,
ptnr2_label_asym_id: asym2,
ptnr2_label_seq_id: seq2,
ptnr2_auth_seq_id: authSeq2,
pdbx_ptnr2_PDB_ins_code: authInsCode2,
ptnr2_label_comp_id: comp2,
ptnr2_label_atom_id: atom2,
pdbx_ptnr2_label_alt_id: alt2,
pdbx_dist_value: distance } = mmcifData.db.struct_conn;
const n = id.rowCount;
const result: { [id: string]: StructConnRecord } = {};
for (let i = 0; i < n; i++) {
const conn: StructConnRecord = {
id: id.value(i),
distance: distance.value(i),
partner1: {
asymId: asym1.value(i),
seqId: seq1.valueKind(i) === Column.ValueKinds.Present ? seq1.value(i) : undefined,
authSeqId: authSeq1.valueKind(i) === Column.ValueKinds.Present ? authSeq1.value(i) : undefined,
insCode: authInsCode1.value(i),
compId: comp1.value(i),
atomId: atom1.value(i),
altId: alt1.value(i),
},
partner2: {
asymId: asym2.value(i),
seqId: seq2.valueKind(i) === Column.ValueKinds.Present ? seq2.value(i) : undefined,
authSeqId: authSeq2.valueKind(i) === Column.ValueKinds.Present ? authSeq2.value(i) : undefined,
insCode: authInsCode2.value(i),
compId: comp2.value(i),
atomId: atom2.value(i),
altId: alt2.value(i),
},
};
result[conn.id] = conn;
}
return result;
}
/** Return MolScript expression for atoms or residues involved in a struct_conn */
function structConnExpression(conn: StructConnRecord, by: 'atoms' | 'residues') {
const { core, struct } = MolScriptBuilder;
const partnerExpressions = [];
for (const partner of [conn.partner1, conn.partner2]) {
const propTests: Parameters<typeof struct.generator.atomGroups>[0] = {
'chain-test': core.rel.eq([struct.atomProperty.macromolecular.label_asym_id(), partner.asymId]),
'group-by': struct.atomProperty.core.operatorName(),
};
if (partner.seqId !== undefined) {
propTests['residue-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_seq_id(), partner.seqId]);
} else if (partner.authSeqId !== undefined) { // for the case of water and carbohydrates (see 5elb, covale3 vs covale5)
propTests['residue-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.auth_seq_id(), partner.authSeqId]),
core.rel.eq([struct.atomProperty.macromolecular.pdbx_PDB_ins_code(), partner.insCode]),
]);
}
if (by === 'residues' && partner.altId !== '') {
propTests['atom-test'] = core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]);
}
if (by === 'atoms') {
propTests['atom-test'] = core.logic.and([
core.rel.eq([struct.atomProperty.macromolecular.label_atom_id(), partner.atomId]),
core.rel.eq([struct.atomProperty.macromolecular.label_alt_id(), partner.altId]),
]);
}
partnerExpressions.push(struct.filter.first([struct.generator.atomGroups(propTests)]));
}
return struct.combinator.merge(partnerExpressions.map(e => struct.modifier.union([e])));
}
/** Create visuals for residues and atoms involved in a struct_conn and zoom on them.
* Return a promise that resolves to the number of involved atoms which were successfully selected (2, 1, or 0).
*/
async function addStructConnInspection(plugin: PluginContext, structNode: StructNode, conn: StructConnRecord): Promise<number> {
const expressionByResidues = structConnExpression(conn, 'residues');
const expressionByAtoms = structConnExpression(conn, 'atoms');
const update = plugin.build();
update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (residues)`, type: { name: 'expression', params: expressionByResidues } },
{ tags: [TAGS.RESIDUE_SEL] }
).apply(
StructureRepresentation3D,
RESIDUES_VISUAL_PARAMS,
{ tags: [TAGS.RESIDUE_REPR] }
);
const atomsSelection = update.to(structNode).apply(
StructureComponent,
{ label: `${conn.id} (atoms)`, type: { name: 'expression', params: expressionByAtoms } },
{ tags: [TAGS.ATOM_SEL] }
);
const atomsVisual = update.to(atomsSelection.ref).apply(
StructureRepresentation3D,
ATOMS_VISUAL_PARAMS,
{ tags: [TAGS.ATOM_REPR] }
);
await update.commit();
plugin.managers.camera.focusRenderObjects(atomsVisual.selector.data?.repr.renderObjects, { extraRadius: EXTRA_RADIUS });
const nSelectedAtoms = atomsSelection.selector.obj?.data?.elementCount ?? 0;
return nSelectedAtoms;
}
/** Remove anything created by `addStructConnInspection` */
async function removeAllStructConnInspections(plugin: PluginContext, structNode: StructNode) {
const selNodes = [
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.RESIDUE_SEL)),
...plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag(TAGS.ATOM_SEL)),
];
const update = plugin.build();
for (const node of selNodes) {
update.delete(node);
}
await update.commit();
}
/** Hide all carbohydrate SNFG visuals */
function hideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
setSubtreeVisibility(plugin.state.data, node.transform.ref, true); // true means hidden
}
}
/** Make visible all carbohydrate SNFG visuals that have been hidden by `hideSnfgNodes` */
function unhideSnfgNodes(plugin: PluginContext, structNode: StructNode) {
const snfgNodes = plugin.state.data.selectQ(q => q.byRef(structNode.transform.ref).subtree().withTag('branched-snfg-3d'));
for (const node of snfgNodes) {
try {
setSubtreeVisibility(plugin.state.data, node.transform.ref, false); // false means visible
} catch {
// this is OK, the node has been removed
}
}
}

View File

@@ -202,7 +202,7 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
}));
} else if (t.name === 'trajectory') {
const [topologyUrl, topologyFormat, topologyIsBinary] = t.params.topology.split('|');
const [coordinatesUrl, coordinatesFormat, coordinatesIsBinary] = t.params.coordinates.split('|');
const [coordinatesUrl, coordinatesFormat] = t.params.coordinates.split('|');
await this.plugin.runTask(this.plugin.state.data.applyAction(LoadTrajectory, {
source: {
@@ -216,7 +216,6 @@ export class ZenodoImportUI extends CollapsableControls<{}, State> {
coordinates: {
url: coordinatesUrl,
format: coordinatesFormat as any,
isBinary: coordinatesIsBinary === 'true',
},
}
}

View File

@@ -0,0 +1,37 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
import { Viewport, cameraProject, cameraUnproject } from '../camera/util';
describe('camera', () => {
it('project/unproject', () => {
const proj = Mat4.perspective(Mat4(), -1, 1, 1, -1, 1, 100);
const invProj = Mat4.invert(Mat4(), proj);
const c = Vec4();
const po = Vec3();
const vp = Viewport.create(0, 0, 100, 100);
const pi = Vec3.create(0, 0, 1);
cameraProject(c, pi, vp, proj);
expect(Vec4.equals(c, Vec4.create(50, 50, 2.020202, -1))).toBe(true);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
Vec3.set(pi, 0.5, 0.5, 1);
cameraProject(c, pi, vp, proj);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
Viewport.set(vp, 50, 50, 100, 100);
Vec3.set(pi, 0.5, 0.5, 1);
cameraProject(c, pi, vp, proj);
cameraUnproject(po, c, vp, invProj);
expect(Vec3.equals(po, pi)).toBe(true);
});
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-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>
@@ -119,11 +119,11 @@ class Camera implements ICamera {
return Camera.targetDistance(radius, this.state.fov, this.viewport.width, this.viewport.height);
}
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3): Partial<Camera.Snapshot> {
getFocus(target: Vec3, radius: number, up?: Vec3, dir?: Vec3, snapshot?: Partial<Camera.Snapshot>): Partial<Camera.Snapshot> {
const r = Math.max(radius, 0.01);
const targetDistance = this.getTargetDistance(r);
Vec3.sub(this.deltaDirection, this.target, this.position);
Vec3.sub(this.deltaDirection, snapshot?.target ?? this.target, snapshot?.position ?? this.position);
if (dir) Vec3.matchDirection(this.deltaDirection, dir, this.deltaDirection);
Vec3.setMagnitude(this.deltaDirection, this.deltaDirection, targetDistance);
Vec3.sub(this.newPosition, target, this.deltaDirection);
@@ -137,6 +137,18 @@ class Camera implements ICamera {
return state;
}
getCenter(target: Vec3, radius?: number): Partial<Camera.Snapshot> {
Vec3.sub(this.deltaDirection, this.target, this.position);
Vec3.sub(this.newPosition, target, this.deltaDirection);
const state = Camera.copySnapshot(Camera.createDefaultSnapshot(), this.state);
state.target = Vec3.clone(target);
state.position = Vec3.clone(this.newPosition);
if (radius) state.radius = Math.max(radius, 0.01);
return state;
}
getInvariantFocus(target: Vec3, radius: number, up: Vec3, dir: Vec3): Partial<Camera.Snapshot> {
const r = Math.max(radius, 0.01);
const targetDistance = this.getTargetDistance(r);
@@ -160,6 +172,10 @@ class Camera implements ICamera {
}
}
center(target: Vec3, durationMs?: number) {
this.setState(this.getCenter(target), durationMs);
}
/** Transform point into 2D window coordinates. */
project(out: Vec4, point: Vec3) {
return cameraProject(out, point, this.viewport, this.projectionView);
@@ -178,7 +194,7 @@ class Camera implements ICamera {
getPixelSize(point: Vec3) {
// project -> unproject of `point` does not exactly return the same
// to get a sufficiently accurate measure we unproject the original
// clip position in addition to the one shifted bey one pixel
// clip position in addition to the one shifted by one pixel
this.project(tmpClip, point);
this.unproject(tmpPos1, tmpClip);
tmpClip[0] += 1;
@@ -262,6 +278,7 @@ namespace Camera {
fog: 50,
clipFar: true,
minNear: 5,
minFar: 0,
};
}
@@ -278,6 +295,7 @@ namespace Camera {
fog: number
clipFar: boolean
minNear: number
minFar: number
}
export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -295,6 +313,7 @@ namespace Camera {
if (typeof source.fog !== 'undefined') out.fog = source.fog;
if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
if (typeof source.minFar !== 'undefined') out.minFar = source.minFar;
return out;
}
@@ -307,6 +326,7 @@ namespace Camera {
&& a.fog === b.fog
&& a.clipFar === b.clipFar
&& a.minNear === b.minNear
&& a.minFar === b.minFar
&& Vec3.exactEquals(a.position, b.position)
&& Vec3.exactEquals(a.up, b.up)
&& Vec3.exactEquals(a.target, b.target);
@@ -374,18 +394,14 @@ function updatePers(camera: Camera) {
}
function updateClip(camera: Camera) {
let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
let { radius, radiusMax, mode, fog, clipFar, minNear, minFar } = camera.state;
if (radius < 0.01) radius = 0.01;
const normalizedFar = clipFar ? radius : radiusMax;
const normalizedFar = Math.max(clipFar ? radius : radiusMax, minFar);
const cameraDistance = Vec3.distance(camera.position, camera.target);
let near = cameraDistance - radius;
let far = cameraDistance + normalizedFar;
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
if (mode === 'perspective') {
// set at least to 5 to avoid slow sphere impostor rendering
near = Math.max(Math.min(radiusMax, minNear), near);
@@ -401,8 +417,12 @@ function updateClip(camera: Camera) {
far = near + 0.01;
}
const fogNearFactor = -(50 - fog) / 50;
const fogNear = cameraDistance - (normalizedFar * fogNearFactor);
const fogFar = far;
camera.near = near;
camera.far = 2 * far; // avoid precision issues distingushing far objects from background
camera.far = far;
camera.fogNear = fogNear;
camera.fogFar = fogFar;
}

View File

@@ -77,7 +77,7 @@ export function cameraProject(out: Vec4, point: Vec3, viewport: Viewport, projec
// transform into window coordinates, set fourth component to 1 / clip.w as in gl_FragCoord.w
out[0] = (tmpVec4[0] + 1) * width * 0.5 + x;
out[1] = (1 - tmpVec4[1]) * height * 0.5 + y; // flip Y
out[1] = (tmpVec4[1] + 1) * height * 0.5 + y;
out[2] = (tmpVec4[2] + 1) * 0.5;
out[3] = w === 0 ? 0 : 1 / w;
return out;
@@ -92,7 +92,7 @@ export function cameraUnproject(out: Vec3, point: Vec3 | Vec4, viewport: Viewpor
const { x, y, width, height } = viewport;
const px = point[0] - x;
const py = (height - point[1] - 1) - y;
const py = point[1] - y;
const pz = point[2];
out[0] = (2 * px) / width - 1;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -31,7 +31,7 @@ import { PickData } from './passes/pick';
import { PickHelper } from './passes/pick';
import { ImagePass, ImageProps } from './passes/image';
import { Sphere3D } from '../mol-math/geometry';
import { isDebugMode, isTimingMode } from '../mol-util/debug';
import { addConsoleStatsProvider, isDebugMode, isTimingMode, removeConsoleStatsProvider } from '../mol-util/debug';
import { CameraHelperParams } from './helper/camera-helper';
import { produce } from 'immer';
import { HandleHelperParams } from './helper/handle-helper';
@@ -65,7 +65,7 @@ export const Canvas3DParams = {
cameraClipping: PD.Group({
radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
minNear: PD.Numeric(5, { min: 0.1, max: 100, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
}, { pivot: 'radius' }),
viewport: PD.MappedStatic('canvas', {
canvas: PD.Group({}),
@@ -120,6 +120,7 @@ interface Canvas3DContext {
namespace Canvas3DContext {
export const DefaultAttribs = {
powerPreference: 'high-performance' as WebGLContextAttributes['powerPreference'],
failIfMajorPerformanceCaveat: false,
/** true by default to avoid issues with Safari (Jan 2021) */
antialias: true,
@@ -140,8 +141,9 @@ namespace Canvas3DContext {
if (a.enableWboit && a.enableDpoit) throw new Error('Multiple transparency methods not allowed.');
const { failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
const { powerPreference, failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
const gl = getGLContext(canvas, {
powerPreference,
failIfMajorPerformanceCaveat,
antialias,
preserveDrawingBuffer,
@@ -258,6 +260,7 @@ interface Canvas3D {
notifyDidDraw: boolean,
readonly didDraw: BehaviorSubject<now.Timestamp>
readonly commited: BehaviorSubject<now.Timestamp>
readonly commitQueueSize: BehaviorSubject<number>
readonly reprCount: BehaviorSubject<number>
readonly resized: BehaviorSubject<any>
@@ -304,6 +307,7 @@ namespace Canvas3D {
let startTime = now();
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
const commited = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
const commitQueueSize = new BehaviorSubject<number>(0);
const { gl, contextRestored } = webgl;
@@ -328,12 +332,12 @@ namespace Canvas3D {
}, { x, y, width, height }, { pixelScale: attribs.pixelScale });
const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
const controls = TrackballControls.create(input, camera, p.trackball);
const controls = TrackballControls.create(input, camera, scene, p.trackball);
const renderer = Renderer.create(webgl, p.renderer);
const helper = new Helper(webgl, scene, p);
const pickHelper = new PickHelper(webgl, renderer, scene, helper, passes.pick, { x, y, width, height }, attribs.pickPadding);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, controls, p.interaction);
const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
@@ -438,7 +442,7 @@ namespace Canvas3D {
cam = stereoCamera;
}
if (isTimingMode) webgl.timer.mark('Canvas3D.render');
if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
const ctx = { renderer, camera: cam, scene, helper };
if (MultiSamplePass.isEnabled(p.multiSample)) {
const forceOn = !cameraChanged && markingUpdated && !controls.isAnimating;
@@ -591,7 +595,11 @@ namespace Canvas3D {
// snapshot the current bounding sphere of visible objects
Sphere3D.copy(oldBoundingSphereVisible, scene.boundingSphereVisible);
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) return false;
if (!scene.commit(isSynchronous ? void 0 : sceneCommitTimeoutMs)) {
commitQueueSize.next(scene.commitQueueSize);
return false;
}
commitQueueSize.next(0);
if (helper.debug.isEnabled) helper.debug.update();
if (!p.camera.manualReset && (reprCount.value === 0 || shouldResetCamera())) {
@@ -607,20 +615,32 @@ namespace Canvas3D {
}
function consoleStats() {
console.table(scene.renderables.map(r => ({
const items = scene.renderables.map(r => ({
drawCount: r.values.drawCount.ref.value,
instanceCount: r.values.instanceCount.ref.value,
materialId: r.materialId,
renderItemId: r.id,
})));
console.log(webgl.stats);
}));
console.groupCollapsed(`${items.length} RenderItems`);
if (items.length < 50) {
console.table(items);
} else {
console.log(items);
}
console.log(JSON.stringify(webgl.stats, undefined, 4));
const { texture, attribute, elements } = webgl.resources.getByteCounts();
console.log({
console.log(JSON.stringify({
texture: `${(texture / 1024 / 1024).toFixed(3)} MiB`,
attribute: `${(attribute / 1024 / 1024).toFixed(3)} MiB`,
elements: `${(elements / 1024 / 1024).toFixed(3)} MiB`,
});
}, undefined, 4));
console.log(JSON.stringify(webgl.timer.formatedStats(), undefined, 4));
console.groupEnd();
}
function add(repr: Representation.Any) {
@@ -726,6 +746,8 @@ namespace Canvas3D {
resized.next(+new Date());
}
addConsoleStatsProvider(consoleStats);
return {
webgl,
@@ -788,6 +810,7 @@ namespace Canvas3D {
set notifyDidDraw(v: boolean) { notifyDidDraw = v; },
didDraw,
commited,
commitQueueSize,
reprCount,
resized,
setProps: (properties, doNotRequestDraw = false) => {
@@ -895,6 +918,7 @@ namespace Canvas3D {
},
dispose: () => {
contextRestoredSub.unsubscribe();
cancelAnimationFrame(animationFrameHandle);
markBuffer = [];
@@ -903,6 +927,8 @@ namespace Canvas3D {
controls.dispose();
renderer.dispose();
interactionHelper.dispose();
removeConsoleStatsProvider(consoleStats);
}
};

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
@@ -10,20 +10,25 @@
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
import { Viewport } from '../camera/util';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput } from '../../mol-util/input/input-observer';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Camera } from '../camera';
import { absMax, degToRad } from '../../mol-math/misc';
import { Binding } from '../../mol-util/binding';
import { Scene } from '../../mol-gl/scene';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
export const DefaultTrackballBindings = {
dragRotate: Binding([Trigger(B.Flag.Primary, M.create())], 'Rotate', 'Drag using ${triggers}'),
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true }))], 'Rotate around z-axis', 'Drag using ${triggers}'),
dragPan: Binding([Trigger(B.Flag.Secondary, M.create()), Trigger(B.Flag.Primary, M.create({ control: true }))], 'Pan', 'Drag using ${triggers}'),
dragRotateZ: Binding([Trigger(B.Flag.Primary, M.create({ shift: true, control: true }))], 'Rotate around z-axis (roll)', 'Drag using ${triggers}'),
dragPan: Binding([
Trigger(B.Flag.Secondary, M.create()),
Trigger(B.Flag.Primary, M.create({ control: true }))
], 'Pan', 'Drag using ${triggers}'),
dragZoom: Binding.Empty,
dragFocus: Binding([Trigger(B.Flag.Forth, M.create())], 'Focus', 'Drag using ${triggers}'),
dragFocusZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Focus and zoom', 'Drag using ${triggers}'),
@@ -31,6 +36,22 @@ export const DefaultTrackballBindings = {
scrollZoom: Binding([Trigger(B.Flag.Auxilary, M.create())], 'Zoom', 'Scroll using ${triggers}'),
scrollFocus: Binding([Trigger(B.Flag.Auxilary, M.create({ shift: true }))], 'Clip', 'Scroll using ${triggers}'),
scrollFocusZoom: Binding.Empty,
keyMoveForward: Binding([Key('KeyW')], 'Move forward', 'Press ${triggers}'),
keyMoveBack: Binding([Key('KeyS')], 'Move back', 'Press ${triggers}'),
keyMoveLeft: Binding([Key('KeyA')], 'Move left', 'Press ${triggers}'),
keyMoveRight: Binding([Key('KeyD')], 'Move right', 'Press ${triggers}'),
keyMoveUp: Binding([Key('KeyR')], 'Move up', 'Press ${triggers}'),
keyMoveDown: Binding([Key('KeyF')], 'Move down', 'Press ${triggers}'),
keyRollLeft: Binding([Key('KeyQ')], 'Roll left', 'Press ${triggers}'),
keyRollRight: Binding([Key('KeyE')], 'Roll right', 'Press ${triggers}'),
keyPitchUp: Binding([Key('ArrowUp', M.create({ shift: true }))], 'Pitch up', 'Press ${triggers}'),
keyPitchDown: Binding([Key('ArrowDown', M.create({ shift: true }))], 'Pitch down', 'Press ${triggers}'),
keyYawLeft: Binding([Key('ArrowLeft', M.create({ shift: true }))], 'Yaw left', 'Press ${triggers}'),
keyYawRight: Binding([Key('ArrowRight', M.create({ shift: true }))], 'Yaw right', 'Press ${triggers}'),
boostMove: Binding([Key('ShiftLeft')], 'Boost move', 'Press ${triggers}'),
enablePointerLock: Binding([Key('Space', M.create({ control: true }))], 'Enable pointer lock', 'Press ${triggers}'),
};
export const TrackballControlsParams = {
@@ -39,6 +60,9 @@ export const TrackballControlsParams = {
rotateSpeed: PD.Numeric(5.0, { min: 1, max: 10, step: 1 }),
zoomSpeed: PD.Numeric(7.0, { min: 1, max: 15, step: 1 }),
panSpeed: PD.Numeric(1.0, { min: 0.1, max: 5, step: 0.1 }),
moveSpeed: PD.Numeric(0.75, { min: 0.1, max: 3, step: 0.1 }),
boostMoveFactor: PD.Numeric(5.0, { min: 0.1, max: 10, step: 0.1 }),
flyMode: PD.Boolean(false),
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
@@ -82,6 +106,7 @@ export { TrackballControls };
interface TrackballControls {
readonly viewport: Viewport
readonly isAnimating: boolean
readonly isMoving: boolean
readonly props: Readonly<TrackballControlsProps>
setProps: (props: Partial<TrackballControlsProps>) => void
@@ -92,8 +117,14 @@ interface TrackballControls {
dispose: () => void
}
namespace TrackballControls {
export function create(input: InputObserver, camera: Camera, props: Partial<TrackballControlsProps> = {}): TrackballControls {
const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props };
export function create(input: InputObserver, camera: Camera, scene: Scene, props: Partial<TrackballControlsProps> = {}): TrackballControls {
const p: TrackballControlsProps = {
...PD.getDefaultValues(TrackballControlsParams),
...props,
// include default bindings for backwards state compatibility
bindings: { ...DefaultTrackballBindings, ...props.bindings }
};
const b = p.bindings;
const viewport = Viewport.clone(camera.viewport);
@@ -104,6 +135,11 @@ namespace TrackballControls {
const wheelSub = input.wheel.subscribe(onWheel);
const pinchSub = input.pinch.subscribe(onPinch);
const gestureSub = input.gesture.subscribe(onGesture);
const keyDownSub = input.keyDown.subscribe(onKeyDown);
const keyUpSub = input.keyUp.subscribe(onKeyUp);
const moveSub = input.move.subscribe(onMove);
const lockSub = input.lock.subscribe(onLock);
const leaveSub = input.leave.subscribe(onLeave);
let _isInteracting = false;
@@ -117,9 +153,12 @@ namespace TrackballControls {
const _rotLastAxis = Vec3();
let _rotLastAngle = 0;
const _zRotPrev = Vec2();
const _zRotCurr = Vec2();
let _zRotLastAngle = 0;
const _rollPrev = Vec2();
const _rollCurr = Vec2();
let _rollLastAngle = 0;
let _pitchLastAngle = 0;
let _yawLastAngle = 0;
const _zoomStart = Vec2();
const _zoomEnd = Vec2();
@@ -149,7 +188,7 @@ namespace TrackballControls {
return Vec2.set(
mouseOnCircleVec2,
(pageX - viewport.width * 0.5 - viewport.x) / (viewport.width * 0.5),
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // screen.width intentional
(viewport.height + 2 * (viewport.y - pageY)) / viewport.width // viewport.width intentional
);
}
@@ -203,26 +242,74 @@ namespace TrackballControls {
Vec2.copy(_rotPrev, _rotCurr);
}
const zRotQuat = Quat();
const rollQuat = Quat();
const rollDir = Vec3();
function zRotateCamera() {
const dx = _zRotCurr[0] - _zRotPrev[0];
const dy = _zRotCurr[1] - _zRotPrev[1];
const angle = p.rotateSpeed * (-dx + dy) * -0.05;
function rollCamera() {
const k = (keyState.rollRight - keyState.rollLeft) / 45;
const dx = (_rollCurr[0] - _rollPrev[0]) * -Math.sign(_rollCurr[1]);
const dy = (_rollCurr[1] - _rollPrev[1]) * -Math.sign(_rollCurr[0]);
const angle = -p.rotateSpeed * (-dx + dy) + k;
if (angle) {
Vec3.sub(_eye, camera.position, camera.target);
Quat.setAxisAngle(zRotQuat, _eye, angle);
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
_zRotLastAngle = angle;
} else if (!p.staticMoving && _zRotLastAngle) {
_zRotLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.sub(_eye, camera.position, camera.target);
Quat.setAxisAngle(zRotQuat, _eye, _zRotLastAngle);
Vec3.transformQuat(camera.up, camera.up, zRotQuat);
Vec3.normalize(rollDir, _eye);
Quat.setAxisAngle(rollQuat, rollDir, angle);
Vec3.transformQuat(camera.up, camera.up, rollQuat);
_rollLastAngle = angle;
} else if (!p.staticMoving && _rollLastAngle) {
_rollLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.normalize(rollDir, _eye);
Quat.setAxisAngle(rollQuat, rollDir, _rollLastAngle);
Vec3.transformQuat(camera.up, camera.up, rollQuat);
}
Vec2.copy(_zRotPrev, _zRotCurr);
Vec2.copy(_rollPrev, _rollCurr);
}
const pitchQuat = Quat();
const pitchDir = Vec3();
function pitchCamera() {
const m = (keyState.pitchUp - keyState.pitchDown) / (p.flyMode ? 360 : 90);
const angle = -p.rotateSpeed * m;
if (angle) {
Vec3.cross(pitchDir, _eye, camera.up);
Vec3.normalize(pitchDir, pitchDir);
Quat.setAxisAngle(pitchQuat, pitchDir, angle);
Vec3.transformQuat(_eye, _eye, pitchQuat);
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
_pitchLastAngle = angle;
} else if (!p.staticMoving && _pitchLastAngle) {
_pitchLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.cross(pitchDir, _eye, camera.up);
Vec3.normalize(pitchDir, pitchDir);
Quat.setAxisAngle(pitchQuat, pitchDir, _pitchLastAngle);
Vec3.transformQuat(_eye, _eye, pitchQuat);
Vec3.transformQuat(camera.up, camera.up, pitchQuat);
}
}
const yawQuat = Quat();
const yawDir = Vec3();
function yawCamera() {
const m = (keyState.yawRight - keyState.yawLeft) / (p.flyMode ? 360 : 90);
const angle = -p.rotateSpeed * m;
if (angle) {
Vec3.normalize(yawDir, camera.up);
Quat.setAxisAngle(yawQuat, yawDir, angle);
Vec3.transformQuat(_eye, _eye, yawQuat);
Vec3.transformQuat(camera.up, camera.up, yawQuat);
_yawLastAngle = angle;
} else if (!p.staticMoving && _yawLastAngle) {
_yawLastAngle *= Math.sqrt(1.0 - p.dynamicDampingFactor);
Vec3.normalize(yawDir, camera.up);
Quat.setAxisAngle(yawQuat, yawDir, _yawLastAngle);
Vec3.transformQuat(_eye, _eye, yawQuat);
Vec3.transformQuat(camera.up, camera.up, yawQuat);
}
}
function zoomCamera() {
@@ -283,6 +370,92 @@ namespace TrackballControls {
}
}
const keyState = {
moveUp: 0, moveDown: 0, moveLeft: 0, moveRight: 0, moveForward: 0, moveBack: 0,
pitchUp: 0, pitchDown: 0, yawLeft: 0, yawRight: 0, rollLeft: 0, rollRight: 0,
boostMove: 0,
};
const moveDir = Vec3();
const moveEye = Vec3();
function moveCamera(deltaT: number) {
Vec3.sub(moveEye, camera.position, camera.target);
const minDistance = Math.max(camera.state.minNear, p.minDistance);
Vec3.setMagnitude(moveEye, moveEye, minDistance);
const moveSpeed = deltaT * (60 / 1000) * p.moveSpeed * (keyState.boostMove === 1 ? p.boostMoveFactor : 1);
if (keyState.moveForward === 1) {
Vec3.normalize(moveDir, moveEye);
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
const dt = Vec3.distance(camera.target, camera.position);
const ds = Vec3.distance(scene.boundingSphereVisible.center, camera.position);
if (p.flyMode || input.pointerLock || (dt < minDistance && ds < camera.state.radiusMax)) {
Vec3.sub(camera.target, camera.position, moveEye);
}
}
if (keyState.moveBack === 1) {
Vec3.normalize(moveDir, moveEye);
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
if (p.flyMode || input.pointerLock) {
Vec3.sub(camera.target, camera.position, moveEye);
}
}
if (keyState.moveLeft === 1) {
Vec3.cross(moveDir, moveEye, camera.up);
Vec3.normalize(moveDir, moveDir);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveRight === 1) {
Vec3.cross(moveDir, moveEye, camera.up);
Vec3.normalize(moveDir, moveDir);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveUp === 1) {
Vec3.normalize(moveDir, camera.up);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (keyState.moveDown === 1) {
Vec3.normalize(moveDir, camera.up);
if (p.flyMode || input.pointerLock) {
Vec3.scaleAndSub(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, moveEye);
} else {
Vec3.scaleAndAdd(camera.position, camera.position, moveDir, moveSpeed);
Vec3.sub(camera.target, camera.position, _eye);
}
}
if (p.flyMode || input.pointerLock) {
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
}
}
/**
* Ensure the distance between object and target is within the min/max distance
* and not too large compared to `camera.state.radiusMax`
@@ -319,15 +492,19 @@ namespace TrackballControls {
/** Update the object's position, direction and up vectors */
function update(t: number) {
if (lastUpdated === t) return;
const deltaT = t - lastUpdated;
if (lastUpdated > 0) {
if (p.animate.name === 'spin') spin(t - lastUpdated);
else if (p.animate.name === 'rock') rock(t - lastUpdated);
if (p.animate.name === 'spin') spin(deltaT);
else if (p.animate.name === 'rock') rock(deltaT);
}
Vec3.sub(_eye, camera.position, camera.target);
rotateCamera();
zRotateCamera();
rollCamera();
pitchCamera();
yawCamera();
zoomCamera();
focusCamera();
panCamera();
@@ -335,6 +512,15 @@ namespace TrackballControls {
Vec3.add(camera.position, camera.target, _eye);
checkDistances();
if (lastUpdated > 0) {
// clamp the maximum step size at 15 frames to avoid too big jumps
// TODO: make this a parameter?
moveCamera(Math.min(deltaT, 15 * 1000 / 60));
}
Vec3.sub(_eye, camera.position, camera.target);
checkDistances();
if (Vec3.squaredDistance(lastPosition, camera.position) > EPSILON) {
Vec3.copy(lastPosition, camera.position);
}
@@ -363,24 +549,28 @@ namespace TrackballControls {
_isInteracting = true;
resetRock(); // start rocking from the center after interactions
const dragRotate = Binding.match(p.bindings.dragRotate, buttons, modifiers);
const dragRotateZ = Binding.match(p.bindings.dragRotateZ, buttons, modifiers);
const dragPan = Binding.match(p.bindings.dragPan, buttons, modifiers);
const dragZoom = Binding.match(p.bindings.dragZoom, buttons, modifiers);
const dragFocus = Binding.match(p.bindings.dragFocus, buttons, modifiers);
const dragFocusZoom = Binding.match(p.bindings.dragFocusZoom, buttons, modifiers);
const dragRotate = Binding.match(b.dragRotate, buttons, modifiers);
const dragRotateZ = Binding.match(b.dragRotateZ, buttons, modifiers);
const dragPan = Binding.match(b.dragPan, buttons, modifiers);
const dragZoom = Binding.match(b.dragZoom, buttons, modifiers);
const dragFocus = Binding.match(b.dragFocus, buttons, modifiers);
const dragFocusZoom = Binding.match(b.dragFocusZoom, buttons, modifiers);
getMouseOnCircle(pageX, pageY);
getMouseOnScreen(pageX, pageY);
const pr = input.pixelRatio;
const vx = (x * pr - viewport.width / 2 - viewport.x) / viewport.width;
const vy = -(input.height - y * pr - viewport.height / 2 - viewport.y) / viewport.height;
if (isStart) {
if (dragRotate) {
Vec2.copy(_rotCurr, mouseOnCircleVec2);
Vec2.copy(_rotPrev, _rotCurr);
}
if (dragRotateZ) {
Vec2.copy(_zRotCurr, mouseOnCircleVec2);
Vec2.copy(_zRotPrev, _zRotCurr);
Vec2.set(_rollCurr, vx, vy);
Vec2.copy(_rollPrev, _rollCurr);
}
if (dragZoom || dragFocusZoom) {
Vec2.copy(_zoomStart, mouseOnScreenVec2);
@@ -397,7 +587,7 @@ namespace TrackballControls {
}
if (dragRotate) Vec2.copy(_rotCurr, mouseOnCircleVec2);
if (dragRotateZ) Vec2.copy(_zRotCurr, mouseOnCircleVec2);
if (dragRotateZ) Vec2.set(_rollCurr, vx, vy);
if (dragZoom || dragFocusZoom) Vec2.copy(_zoomEnd, mouseOnScreenVec2);
if (dragFocus) Vec2.copy(_focusEnd, mouseOnScreenVec2);
if (dragFocusZoom) {
@@ -418,16 +608,16 @@ namespace TrackballControls {
if (delta < -p.maxWheelDelta) delta = -p.maxWheelDelta;
else if (delta > p.maxWheelDelta) delta = p.maxWheelDelta;
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
_zoomEnd[1] += delta;
}
if (Binding.match(p.bindings.scrollFocus, buttons, modifiers)) {
if (Binding.match(b.scrollFocus, buttons, modifiers)) {
_focusEnd[1] += delta;
}
}
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
if (Binding.match(p.bindings.scrollZoom, buttons, modifiers)) {
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
_isInteracting = true;
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
}
@@ -438,6 +628,177 @@ namespace TrackballControls {
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
}
function onMove({ movementX, movementY }: MoveInput) {
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
const cx = viewport.width * 0.5 - viewport.x;
const cy = viewport.height * 0.5 - viewport.y;
Vec2.copy(_rotPrev, getMouseOnCircle(cx, cy));
Vec2.copy(_rotCurr, getMouseOnCircle(movementX + cx, movementY + cy));
}
function onKeyDown({ modifiers, code, x, y }: KeyInput) {
if (outsideViewport(x, y)) return;
if (Binding.matchKey(b.keyMoveForward, code, modifiers)) {
keyState.moveForward = 1;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers)) {
keyState.moveBack = 1;
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers)) {
keyState.moveLeft = 1;
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers)) {
keyState.moveRight = 1;
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers)) {
keyState.moveUp = 1;
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers)) {
keyState.moveDown = 1;
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers)) {
keyState.rollLeft = 1;
} else if (Binding.matchKey(b.keyRollRight, code, modifiers)) {
keyState.rollRight = 1;
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers)) {
keyState.pitchUp = 1;
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers)) {
keyState.pitchDown = 1;
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers)) {
keyState.yawLeft = 1;
} else if (Binding.matchKey(b.keyYawRight, code, modifiers)) {
keyState.yawRight = 1;
}
if (Binding.matchKey(b.boostMove, code, modifiers)) {
keyState.boostMove = 1;
}
if (Binding.matchKey(b.enablePointerLock, code, modifiers)) {
input.requestPointerLock(viewport);
}
}
function onKeyUp({ modifiers, code, x, y }: KeyInput) {
if (outsideViewport(x, y)) return;
let isModifierCode = false;
if (code.startsWith('Alt')) {
isModifierCode = true;
modifiers.alt = true;
} else if (code.startsWith('Shift')) {
isModifierCode = true;
modifiers.shift = true;
} else if (code.startsWith('Control')) {
isModifierCode = true;
modifiers.control = true;
} else if (code.startsWith('Meta')) {
isModifierCode = true;
modifiers.meta = true;
}
const codes = [];
if (isModifierCode) {
if (keyState.moveForward) codes.push(b.keyMoveForward.triggers[0]?.code || '');
if (keyState.moveBack) codes.push(b.keyMoveBack.triggers[0]?.code || '');
if (keyState.moveLeft) codes.push(b.keyMoveLeft.triggers[0]?.code || '');
if (keyState.moveRight) codes.push(b.keyMoveRight.triggers[0]?.code || '');
if (keyState.moveUp) codes.push(b.keyMoveUp.triggers[0]?.code || '');
if (keyState.moveDown) codes.push(b.keyMoveDown.triggers[0]?.code || '');
if (keyState.rollLeft) codes.push(b.keyRollLeft.triggers[0]?.code || '');
if (keyState.rollRight) codes.push(b.keyRollRight.triggers[0]?.code || '');
if (keyState.pitchUp) codes.push(b.keyPitchUp.triggers[0]?.code || '');
if (keyState.pitchDown) codes.push(b.keyPitchDown.triggers[0]?.code || '');
if (keyState.yawLeft) codes.push(b.keyYawLeft.triggers[0]?.code || '');
if (keyState.yawRight) codes.push(b.keyYawRight.triggers[0]?.code || '');
} else {
codes.push(code);
}
for (const code of codes) {
if (Binding.matchKey(b.keyMoveForward, code, modifiers)) {
keyState.moveForward = 0;
} else if (Binding.matchKey(b.keyMoveBack, code, modifiers)) {
keyState.moveBack = 0;
} else if (Binding.matchKey(b.keyMoveLeft, code, modifiers)) {
keyState.moveLeft = 0;
} else if (Binding.matchKey(b.keyMoveRight, code, modifiers)) {
keyState.moveRight = 0;
} else if (Binding.matchKey(b.keyMoveUp, code, modifiers)) {
keyState.moveUp = 0;
} else if (Binding.matchKey(b.keyMoveDown, code, modifiers)) {
keyState.moveDown = 0;
} else if (Binding.matchKey(b.keyRollLeft, code, modifiers)) {
keyState.rollLeft = 0;
} else if (Binding.matchKey(b.keyRollRight, code, modifiers)) {
keyState.rollRight = 0;
} else if (Binding.matchKey(b.keyPitchUp, code, modifiers)) {
keyState.pitchUp = 0;
} else if (Binding.matchKey(b.keyPitchDown, code, modifiers)) {
keyState.pitchDown = 0;
} else if (Binding.matchKey(b.keyYawLeft, code, modifiers)) {
keyState.yawLeft = 0;
} else if (Binding.matchKey(b.keyYawRight, code, modifiers)) {
keyState.yawRight = 0;
}
}
if (Binding.matchKey(b.boostMove, code, modifiers)) {
keyState.boostMove = 0;
}
}
function initCameraMove() {
Vec3.sub(moveEye, camera.position, camera.target);
const minDistance = Math.max(camera.state.minNear, p.minDistance);
Vec3.setMagnitude(moveEye, moveEye, minDistance);
Vec3.sub(camera.target, camera.position, moveEye);
const cameraDistance = Vec3.distance(camera.position, scene.boundingSphereVisible.center);
camera.setState({ minFar: cameraDistance + scene.boundingSphereVisible.radius });
}
function resetCameraMove() {
const { center, radius } = scene.boundingSphereVisible;
const cameraDistance = Vec3.distance(camera.position, center);
if (cameraDistance > radius) {
const focus = camera.getFocus(center, radius);
camera.setState({ ...focus, minFar: 0 });
} else {
camera.setState({
minFar: 0,
radius: scene.boundingSphereVisible.radius,
});
}
}
function onLock(isLocked: boolean) {
if (isLocked) {
initCameraMove();
} else {
resetCameraMove();
}
}
function unsetKeyState() {
keyState.moveForward = 0;
keyState.moveBack = 0;
keyState.moveLeft = 0;
keyState.moveRight = 0;
keyState.moveUp = 0;
keyState.moveDown = 0;
keyState.rollLeft = 0;
keyState.rollRight = 0;
keyState.pitchUp = 0;
keyState.pitchDown = 0;
keyState.yawLeft = 0;
keyState.yawRight = 0;
keyState.boostMove = 0;
}
function onLeave() {
unsetKeyState();
}
function dispose() {
if (disposed) return;
disposed = true;
@@ -447,6 +808,11 @@ namespace TrackballControls {
pinchSub.unsubscribe();
gestureSub.unsubscribe();
interactionEndSub.unsubscribe();
keyDownSub.unsubscribe();
keyUpSub.unsubscribe();
moveSub.unsubscribe();
lockSub.unsubscribe();
leaveSub.unsubscribe();
}
const _spinSpeed = Vec2.create(0.005, 0);
@@ -489,13 +855,31 @@ namespace TrackballControls {
return {
viewport,
get isAnimating() { return p.animate.name !== 'off'; },
get isMoving() {
return (
keyState.moveForward === 1 || keyState.moveBack === 1 ||
keyState.moveLeft === 1 || keyState.moveRight === 1 ||
keyState.moveUp === 1 || keyState.moveDown === 1 ||
keyState.rollLeft === 1 || keyState.rollRight === 1 ||
keyState.pitchUp === 1 || keyState.pitchDown === 1 ||
keyState.yawLeft === 1 || keyState.yawRight === 1
);
},
get props() { return p as Readonly<TrackballControlsProps>; },
setProps: (props: Partial<TrackballControlsProps>) => {
if (props.animate?.name === 'rock' && p.animate.name !== 'rock') {
resetRock(); // start rocking from the center
}
if (props.flyMode !== undefined && props.flyMode !== p.flyMode) {
if (props.flyMode) {
initCameraMove();
} else {
resetCameraMove();
}
}
Object.assign(p, props);
Object.assign(b, props.bindings);
},
start,

View File

@@ -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>
*/
@@ -11,6 +11,8 @@ import { addSphere } from '../../mol-geo/geometry/mesh/builder/sphere';
import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../mol-geo/geometry/mesh/mesh-builder';
import { PickingId } from '../../mol-geo/geometry/picking';
import { Text } from '../../mol-geo/geometry/text/text';
import { TextBuilder } from '../../mol-geo/geometry/text/text-builder';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { Scene } from '../../mol-gl/scene';
import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -23,19 +25,36 @@ import { Visual } from '../../mol-repr/visual';
import { ColorNames } from '../../mol-util/color/names';
import { MarkerAction, MarkerActions } from '../../mol-util/marker-action';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { Camera, ICamera } from '../camera';
import { Viewport } from '../camera/util';
// TODO add scale line/grid
const AxesParams = {
...Mesh.Params,
alpha: { ...Mesh.Params.alpha, defaultValue: 0.51 },
ignoreLight: { ...Mesh.Params.ignoreLight, defaultValue: true },
alpha: PD.Numeric(0.51, { min: 0, max: 1, step: 0.01 }, { isEssential: true, label: 'Opacity' }),
colorX: PD.Color(ColorNames.red, { isEssential: true }),
colorY: PD.Color(ColorNames.green, { isEssential: true }),
colorZ: PD.Color(ColorNames.blue, { isEssential: true }),
scale: PD.Numeric(0.33, { min: 0.1, max: 2, step: 0.1 }, { isEssential: true }),
location: PD.Select('bottom-left', PD.arrayToOptions(['bottom-left', 'bottom-right', 'top-left', 'top-right'] as const)),
locationOffsetX: PD.Numeric(0),
locationOffsetY: PD.Numeric(0),
originColor: PD.Color(ColorNames.grey),
radiusScale: PD.Numeric(0.075, { min: 0.01, max: 0.3, step: 0.001 }),
showPlanes: PD.Boolean(true),
planeColorXY: PD.Color(ColorNames.grey, { label: 'Plane Color XY' }),
planeColorXZ: PD.Color(ColorNames.grey, { label: 'Plane Color XZ' }),
planeColorYZ: PD.Color(ColorNames.grey, { label: 'Plane Color YZ' }),
showLabels: PD.Boolean(false),
labelX: PD.Text('X'),
labelY: PD.Text('Y'),
labelZ: PD.Text('Z'),
labelColorX: PD.Color(ColorNames.grey),
labelColorY: PD.Color(ColorNames.grey),
labelColorZ: PD.Color(ColorNames.grey),
labelOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
labelScale: PD.Numeric(0.25, { min: 0.1, max: 1.0, step: 0.01 }),
};
type AxesParams = typeof AxesParams
type AxesProps = PD.Values<AxesParams>
@@ -56,7 +75,9 @@ export class CameraHelper {
axes: { name: 'off', params: {} }
};
private renderObject: GraphicsRenderObject | undefined;
private meshRenderObject: GraphicsRenderObject | undefined;
private textRenderObject: GraphicsRenderObject | undefined;
private pixelRatio = 1;
constructor(private webgl: WebGLContext, props: Partial<CameraHelperProps> = {}) {
this.scene = Scene.create(webgl, GraphicsRenderVariantsBlended);
@@ -74,9 +95,20 @@ export class CameraHelper {
p.axes.name = props.axes.name;
if (props.axes.name === 'on') {
this.scene.clear();
const params = { ...props.axes.params, scale: props.axes.params.scale * this.webgl.pixelRatio };
this.renderObject = createAxesRenderObject(params);
this.scene.add(this.renderObject);
this.pixelRatio = this.webgl.pixelRatio;
const params = {
...props.axes.params,
scale: props.axes.params.scale * this.pixelRatio,
labelScale: props.axes.params.labelScale * this.pixelRatio,
};
this.meshRenderObject = createMeshRenderObject(params);
this.scene.add(this.meshRenderObject);
if (props.axes.params.showLabels) {
this.textRenderObject = createTextRenderObject(params);
this.scene.add(this.textRenderObject);
} else {
this.textRenderObject = undefined;
}
this.scene.commit();
Vec3.set(this.camera.position, 0, 0, params.scale * 200);
@@ -94,19 +126,29 @@ export class CameraHelper {
getLoci(pickingId: PickingId) {
const { objectId, groupId, instanceId } = pickingId;
if (!this.renderObject || objectId !== this.renderObject.id || groupId === CameraHelperAxis.None) return EmptyLoci;
if ((
(!this.meshRenderObject || objectId !== this.meshRenderObject.id) &&
(!this.textRenderObject || objectId !== this.textRenderObject.id)
) || groupId === CameraHelperAxis.None) return EmptyLoci;
return CameraAxesLoci(this, groupId, instanceId);
}
private eachGroup = (loci: Loci, apply: (interval: Interval) => boolean): boolean => {
if (!this.renderObject) return false;
if (!isCameraAxesLoci(loci)) return false;
let changed = false;
const groupCount = this.renderObject.values.uGroupCount.ref.value;
const { elements } = loci;
for (const { groupId, instanceId } of elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
if (this.meshRenderObject) {
const groupCount = this.meshRenderObject.values.uGroupCount.ref.value;
for (const { groupId, instanceId } of loci.elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
}
if (this.textRenderObject) {
const groupCount = this.textRenderObject.values.uGroupCount.ref.value;
for (const { groupId, instanceId } of loci.elements) {
const idx = instanceId * groupCount + groupId;
if (apply(Interval.ofSingleton(idx))) changed = true;
}
}
return changed;
};
@@ -117,42 +159,82 @@ export class CameraHelper {
if (!isCameraAxesLoci(loci)) return false;
if (loci.data !== this) return false;
}
return Visual.mark(this.renderObject, loci, action, this.eachGroup);
return (
Visual.mark(this.meshRenderObject, loci, action, this.eachGroup) ||
Visual.mark(this.textRenderObject, loci, action, this.eachGroup)
);
}
update(camera: ICamera) {
if (!this.renderObject) return;
if (!this.meshRenderObject || this.props.axes.name === 'off') return;
if (this.pixelRatio !== this.webgl.pixelRatio) {
this.setProps(this.props);
}
updateCamera(this.camera, camera.viewport, camera.viewOffset);
Mat4.extractRotation(this.scene.view, camera.view);
const r = this.renderObject.values.boundingSphere.ref.value.radius;
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r,
-camera.viewport.height / 2 + r,
0
));
const r = this.textRenderObject
? this.textRenderObject.values.boundingSphere.ref.value.radius
: this.meshRenderObject.values.boundingSphere.ref.value.radius;
const l = this.props.axes.params.location;
const ox = this.props.axes.params.locationOffsetX * this.pixelRatio;
const oy = this.props.axes.params.locationOffsetY * this.pixelRatio;
if (l === 'bottom-left') {
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r + ox,
-camera.viewport.height / 2 + r + oy,
0
));
} else if (l === 'bottom-right') {
Mat4.setTranslation(this.scene.view, Vec3.create(
camera.viewport.width / 2 - r - ox,
-camera.viewport.height / 2 + r + oy,
0
));
} else if (l === 'top-left') {
Mat4.setTranslation(this.scene.view, Vec3.create(
-camera.viewport.width / 2 + r + ox,
camera.viewport.height / 2 - r - oy,
0
));
} else if (l === 'top-right') {
Mat4.setTranslation(this.scene.view, Vec3.create(
camera.viewport.width / 2 - r - ox,
camera.viewport.height / 2 - r - oy,
0
));
} else {
assertUnreachable(l);
}
}
}
export const enum CameraHelperAxis {
export enum CameraHelperAxis {
None = 0,
X,
Y,
Z,
XY,
XZ,
YZ
YZ,
Origin
}
function getAxisLabel(axis: number) {
function getAxisLabel(axis: number, cameraHelper: CameraHelper) {
const a = cameraHelper.props.axes;
const x = a.name === 'on' ? a.params.labelX : 'X';
const y = a.name === 'on' ? a.params.labelY : 'Y';
const z = a.name === 'on' ? a.params.labelZ : 'Z';
switch (axis) {
case CameraHelperAxis.X: return 'X Axis';
case CameraHelperAxis.Y: return 'Y Axis';
case CameraHelperAxis.Z: return 'Z Axis';
case CameraHelperAxis.XY: return 'XY Plane';
case CameraHelperAxis.XZ: return 'XZ Plane';
case CameraHelperAxis.YZ: return 'YZ Plane';
case CameraHelperAxis.X: return `${x} Axis`;
case CameraHelperAxis.Y: return `${y} Axis`;
case CameraHelperAxis.Z: return `${z} Axis`;
case CameraHelperAxis.XY: return `${x}${y} Plane`;
case CameraHelperAxis.XZ: return `${x}${z} Plane`;
case CameraHelperAxis.YZ: return `${y}${z} Plane`;
case CameraHelperAxis.Origin: return 'Origin';
default: return 'Axes';
}
}
@@ -160,7 +242,7 @@ function getAxisLabel(axis: number) {
function CameraAxesLoci(cameraHelper: CameraHelper, groupId: number, instanceId: number) {
return DataLoci('camera-axes', cameraHelper, [{ groupId, instanceId }],
void 0 /** bounding sphere */,
() => getAxisLabel(groupId));
() => getAxisLabel(groupId, cameraHelper));
}
export type CameraAxesLoci = ReturnType<typeof CameraAxesLoci>
export function isCameraAxesLoci(x: Loci): x is CameraAxesLoci {
@@ -197,15 +279,17 @@ function updateCamera(camera: Camera, viewport: Viewport, viewOffset: Camera.Vie
Mat4.ortho(camera.projection, left, right, top, bottom, near, far);
}
function createAxesMesh(scale: number, mesh?: Mesh) {
function createAxesMesh(props: AxesProps, mesh?: Mesh) {
const state = MeshBuilder.createState(512, 256, mesh);
const radius = 0.075 * scale;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
const scale = 100 * props.scale;
const radius = props.radiusScale * scale;
const textScale = props.showLabels ? 100 * props.labelScale / 3 : 0;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale - textScale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale - textScale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale - textScale);
const cylinderProps = { radiusTop: radius, radiusBottom: radius, radialSegments: 32 };
state.currentGroup = CameraHelperAxis.None;
state.currentGroup = CameraHelperAxis.Origin;
addSphere(state, Vec3.origin, radius, 2);
state.currentGroup = CameraHelperAxis.X;
@@ -220,50 +304,102 @@ function createAxesMesh(scale: number, mesh?: Mesh) {
addSphere(state, z, radius, 2);
addCylinder(state, Vec3.origin, z, 1, cylinderProps);
Vec3.scale(x, x, 0.5);
Vec3.scale(y, y, 0.5);
Vec3.scale(z, z, 0.5);
if (props.showPlanes) {
Vec3.scale(x, x, 0.5);
Vec3.scale(y, y, 0.5);
Vec3.scale(z, z, 0.5);
state.currentGroup = CameraHelperAxis.XY;
MeshBuilder.addTriangle(state, Vec3.origin, x, y);
MeshBuilder.addTriangle(state, Vec3.origin, y, x);
const xy = Vec3.add(Vec3(), x, y);
MeshBuilder.addTriangle(state, xy, x, y);
MeshBuilder.addTriangle(state, xy, y, x);
state.currentGroup = CameraHelperAxis.XY;
MeshBuilder.addTriangle(state, Vec3.origin, x, y);
MeshBuilder.addTriangle(state, Vec3.origin, y, x);
const xy = Vec3.add(Vec3(), x, y);
MeshBuilder.addTriangle(state, xy, x, y);
MeshBuilder.addTriangle(state, xy, y, x);
state.currentGroup = CameraHelperAxis.XZ;
MeshBuilder.addTriangle(state, Vec3.origin, x, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, x);
const xz = Vec3.add(Vec3(), x, z);
MeshBuilder.addTriangle(state, xz, x, z);
MeshBuilder.addTriangle(state, xz, z, x);
state.currentGroup = CameraHelperAxis.XZ;
MeshBuilder.addTriangle(state, Vec3.origin, x, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, x);
const xz = Vec3.add(Vec3(), x, z);
MeshBuilder.addTriangle(state, xz, x, z);
MeshBuilder.addTriangle(state, xz, z, x);
state.currentGroup = CameraHelperAxis.YZ;
MeshBuilder.addTriangle(state, Vec3.origin, y, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, y);
const yz = Vec3.add(Vec3(), y, z);
MeshBuilder.addTriangle(state, yz, y, z);
MeshBuilder.addTriangle(state, yz, z, y);
state.currentGroup = CameraHelperAxis.YZ;
MeshBuilder.addTriangle(state, Vec3.origin, y, z);
MeshBuilder.addTriangle(state, Vec3.origin, z, y);
const yz = Vec3.add(Vec3(), y, z);
MeshBuilder.addTriangle(state, yz, y, z);
MeshBuilder.addTriangle(state, yz, z, y);
}
return MeshBuilder.getMesh(state);
}
function getAxesShape(props: AxesProps, shape?: Shape<Mesh>) {
function getAxesMeshShape(props: AxesProps, shape?: Shape<Mesh>) {
const scale = 100 * props.scale;
const mesh = createAxesMesh(scale, shape?.geometry);
const mesh = createAxesMesh(props, shape?.geometry);
mesh.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale + scale / 4));
const getColor = (groupId: number) => {
switch (groupId) {
case 1: return props.colorX;
case 2: return props.colorY;
case 3: return props.colorZ;
case CameraHelperAxis.X: return props.colorX;
case CameraHelperAxis.Y: return props.colorY;
case CameraHelperAxis.Z: return props.colorZ;
case CameraHelperAxis.XY: return props.planeColorXY;
case CameraHelperAxis.XZ: return props.planeColorXZ;
case CameraHelperAxis.YZ: return props.planeColorYZ;
case CameraHelperAxis.Origin: return props.originColor;
default: return ColorNames.grey;
}
};
return Shape.create('axes', {}, mesh, getColor, () => 1, () => '');
return Shape.create('axes-mesh', {}, mesh, getColor, () => 1, () => '');
}
function createAxesRenderObject(props: AxesProps) {
const shape = getAxesShape(props);
return Shape.createRenderObject(shape, props);
}
function createMeshRenderObject(props: AxesProps) {
const shape = getAxesMeshShape(props);
return Shape.createRenderObject(shape, {
...PD.getDefaultValues(Mesh.Params),
...props,
ignoreLight: true,
});
}
//
function createAxesText(props: AxesProps, text?: Text) {
const builder = TextBuilder.create(props, 8, 8, text);
const scale = 100 * props.scale;
const x = Vec3.scale(Vec3(), Vec3.unitX, scale);
const y = Vec3.scale(Vec3(), Vec3.unitY, scale);
const z = Vec3.scale(Vec3(), Vec3.unitZ, scale);
const textScale = 100 * props.labelScale;
builder.add(props.labelX, x[0], x[1], x[2], 0.0, textScale, CameraHelperAxis.X);
builder.add(props.labelY, y[0], y[1], y[2], 0.0, textScale, CameraHelperAxis.Y);
builder.add(props.labelZ, z[0], z[1], z[2], 0.0, textScale, CameraHelperAxis.Z);
return builder.getText();
}
function getAxesTextShape(props: AxesProps, shape?: Shape<Text>) {
const scale = 100 * props.scale;
const text = createAxesText(props, shape?.geometry);
text.setBoundingSphere(Sphere3D.create(Vec3.create(scale / 2, scale / 2, scale / 2), scale));
const getColor = (groupId: number) => {
switch (groupId) {
case CameraHelperAxis.X: return props.labelColorX;
case CameraHelperAxis.Y: return props.labelColorY;
case CameraHelperAxis.Z: return props.labelColorZ;
default: return ColorNames.grey;
}
};
return Shape.create('axes-text', {}, text, getColor, () => 1, () => '');
}
function createTextRenderObject(props: AxesProps) {
const shape = getAxesTextShape(props);
return Shape.createRenderObject(shape, {
...PD.getDefaultValues(Text.Params),
...props,
alpha: props.labelOpacity,
});
}

View File

@@ -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>
*/
@@ -54,6 +54,7 @@ export class HandleHelper {
};
private renderObject: GraphicsRenderObject | undefined;
private pixelRatio = 1;
private _transform = Mat4();
getBoundingSphere(out: Sphere3D, instanceId: number) {
@@ -71,7 +72,11 @@ export class HandleHelper {
p.handle.name = props.handle.name;
if (props.handle.name === 'on') {
this.scene.clear();
const params = { ...props.handle.params, scale: props.handle.params.scale * this.webgl.pixelRatio };
this.pixelRatio = this.webgl.pixelRatio;
const params = {
...props.handle.params,
scale: props.handle.params.scale * this.webgl.pixelRatio
};
this.renderObject = createHandleRenderObject(params);
this.scene.add(this.renderObject);
this.scene.commit();
@@ -91,6 +96,10 @@ export class HandleHelper {
update(camera: Camera, position: Vec3, rotation: Mat3) {
if (!this.renderObject) return;
if (this.pixelRatio !== this.webgl.pixelRatio) {
this.setProps(this.props);
}
Mat4.setTranslation(this.renderObject.values.aTransform.ref.value as unknown as Mat4, position);
Mat4.fromMat3(this.renderObject.values.aTransform.ref.value as unknown as Mat4, rotation);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-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>
@@ -13,13 +13,14 @@ import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { Camera } from '../camera';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { Bond } from '../../mol-model/structure';
import { TrackballControls } from '../controls/trackball';
type Canvas3D = import('../canvas3d').Canvas3D
type HoverEvent = import('../canvas3d').Canvas3D.HoverEvent
type DragEvent = import('../canvas3d').Canvas3D.DragEvent
type ClickEvent = import('../canvas3d').Canvas3D.ClickEvent
const enum InputEvent { Move, Click, Drag }
enum InputEvent { Move, Click, Drag }
const tmpPosA = Vec3();
const tmpPos = Vec3();
@@ -68,7 +69,7 @@ export class Canvas3dInteractionHelper {
}
private identify(e: InputEvent, t: number) {
const xyChanged = this.startX !== this.endX || this.startY !== this.endY;
const xyChanged = this.startX !== this.endX || this.startY !== this.endY || (this.input.pointerLock && !this.controls.isMoving);
if (e === InputEvent.Drag) {
if (xyChanged && !this.outsideViewport(this.startX, this.startY)) {
@@ -188,7 +189,7 @@ export class Canvas3dInteractionHelper {
this.ev.dispose();
}
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, props: Partial<Canvas3dInteractionHelperProps> = {}) {
constructor(private canvasIdentify: Canvas3D['identify'], private lociGetter: Canvas3D['getLoci'], private input: InputObserver, private camera: Camera, private controls: TrackballControls, props: Partial<Canvas3dInteractionHelperProps> = {}) {
this.props = { ...PD.getDefaultValues(Canvas3dInteractionHelperParams), ...props };
input.drag.subscribe(({ x, y, buttons, button, modifiers }) => {
@@ -197,8 +198,12 @@ export class Canvas3dInteractionHelper {
this.drag(x, y, buttons, button, modifiers);
});
input.move.subscribe(({ x, y, inside, buttons, button, modifiers }) => {
input.move.subscribe(({ x, y, inside, buttons, button, modifiers, onElement }) => {
if (!inside || this.isInteracting) return;
if (!onElement) {
this.leave();
return;
}
// console.log('move');
this.move(x, y, buttons, button, modifiers);
});

View File

@@ -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 Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
@@ -142,7 +142,7 @@ export class DrawPass {
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -150,7 +150,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
}
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
@@ -196,7 +196,7 @@ export class DrawPass {
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -204,7 +204,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
}
// render transparent primitives and volumes
@@ -260,7 +260,7 @@ export class DrawPass {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
}
if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -268,7 +268,7 @@ export class DrawPass {
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps);
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
@@ -312,7 +312,7 @@ export class DrawPass {
const { x, y, width, height } = camera.viewport;
renderer.setViewport(x, y, width, height);
renderer.update(camera);
renderer.update(camera, scene);
if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
this.drawTarget.bind();
@@ -360,7 +360,7 @@ export class DrawPass {
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}

View File

@@ -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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -27,6 +27,8 @@ export const MarkingParams = {
highlightEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(1.0, 0.4, 0.6), 1.0)),
selectEdgeColor: PD.Color(Color.darken(Color.fromNormalizedRgb(0.2, 1.0, 0.1), 1.0)),
edgeScale: PD.Numeric(1, { min: 1, max: 3, step: 1 }, { description: 'Thickness of the edge.' }),
highlightEdgeStrength: PD.Numeric(1.0, { min: 0, max: 1, step: 0.1 }),
selectEdgeStrength: PD.Numeric(1.0, { min: 0, max: 1, step: 0.1 }),
ghostEdgeStrength: PD.Numeric(0.3, { min: 0, max: 1, step: 0.1 }, { description: 'Opacity of the hidden edges that are covered by other geometry. When set to 1, one less geometry render pass is done.' }),
innerEdgeFactor: PD.Numeric(1.5, { min: 0, max: 3, step: 0.1 }, { description: 'Factor to multiply the inner edge color with - for added contrast.' }),
};
@@ -101,10 +103,10 @@ export class MarkingPass {
}
update(props: MarkingProps) {
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength } = props;
const { highlightEdgeColor, selectEdgeColor, edgeScale, innerEdgeFactor, ghostEdgeStrength, highlightEdgeStrength, selectEdgeStrength } = props;
const { values: edgeValues } = this.edge;
const _edgeScale = Math.round(edgeScale * this.webgl.pixelRatio);
const _edgeScale = Math.max(1, Math.round(edgeScale * this.webgl.pixelRatio));
if (edgeValues.dEdgeScale.ref.value !== _edgeScale) {
ValueCell.update(edgeValues.dEdgeScale, _edgeScale);
this.edge.update();
@@ -113,8 +115,10 @@ export class MarkingPass {
const { values: overlayValues } = this.overlay;
ValueCell.update(overlayValues.uHighlightEdgeColor, Color.toVec3Normalized(overlayValues.uHighlightEdgeColor.ref.value, highlightEdgeColor));
ValueCell.update(overlayValues.uSelectEdgeColor, Color.toVec3Normalized(overlayValues.uSelectEdgeColor.ref.value, selectEdgeColor));
ValueCell.update(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
ValueCell.update(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
ValueCell.updateIfChanged(overlayValues.uInnerEdgeFactor, innerEdgeFactor);
ValueCell.updateIfChanged(overlayValues.uGhostEdgeStrength, ghostEdgeStrength);
ValueCell.updateIfChanged(overlayValues.uHighlightEdgeStrength, highlightEdgeStrength);
ValueCell.updateIfChanged(overlayValues.uSelectEdgeStrength, selectEdgeStrength);
}
render(viewport: Viewport, target: RenderTarget | undefined) {
@@ -170,6 +174,8 @@ const OverlaySchema = {
uTexSizeInv: UniformSpec('v2'),
uHighlightEdgeColor: UniformSpec('v3'),
uSelectEdgeColor: UniformSpec('v3'),
uHighlightEdgeStrength: UniformSpec('f'),
uSelectEdgeStrength: UniformSpec('f'),
uGhostEdgeStrength: UniformSpec('f'),
uInnerEdgeFactor: UniformSpec('f'),
};
@@ -186,6 +192,8 @@ function getOverlayRenderable(ctx: WebGLContext, edgeTexture: Texture): OverlayR
uTexSizeInv: ValueCell.create(Vec2.create(1 / width, 1 / height)),
uHighlightEdgeColor: ValueCell.create(Vec3()),
uSelectEdgeColor: ValueCell.create(Vec3()),
uHighlightEdgeStrength: ValueCell.create(1),
uSelectEdgeStrength: ValueCell.create(1),
uGhostEdgeStrength: ValueCell.create(0),
uInnerEdgeFactor: ValueCell.create(0),
};

View File

@@ -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>
*/
@@ -172,7 +172,7 @@ export class PickPass {
private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: 'pick' | 'depth', pickType: number) {
renderer.clear(false);
renderer.update(camera);
renderer.update(camera, scene);
renderer.renderPick(scene.primitives, camera, variant, null, pickType);
if (helper.handle.isEnabled) {
@@ -181,7 +181,7 @@ export class PickPass {
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, null, pickType);
}
}
@@ -291,7 +291,7 @@ export class PickHelper {
}
private render(camera: Camera | StereoCamera) {
if (isTimingMode) this.webgl.timer.mark('PickHelper.render');
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', true);
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
const { renderer, scene, helper } = this;
@@ -358,7 +358,7 @@ export class PickHelper {
const z = this.getDepth(xp, yp);
// console.log('z', z);
const position = Vec3.create(x, viewport.height - y, z);
const position = Vec3.create(x, y, z);
if (StereoCamera.is(camera)) {
const halfWidth = Math.floor(viewport.width / 2);
if (x > viewport.x + halfWidth) {

View File

@@ -1,8 +1,9 @@
/**
* 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 Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
@@ -10,7 +11,7 @@ import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/rende
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { ValueCell } from '../../mol-util';
import { deepEqual, ValueCell } from '../../mol-util';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
@@ -30,6 +31,8 @@ import { SmaaParams, SmaaPass } from './smaa';
import { isTimingMode } from '../../mol-util/debug';
import { BackgroundParams, BackgroundPass } from './background';
import { AssetManager } from '../../mol-util/assets';
import { Light } from '../../mol-gl/renderer';
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
const OutlinesSchema = {
...QuadSchema,
@@ -40,12 +43,14 @@ const OutlinesSchema = {
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture): OutlinesRenderable {
function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
@@ -58,8 +63,10 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...OutlinesSchema };
@@ -69,9 +76,69 @@ function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, d
return createComputeRenderable(renderItem, values);
}
const ShadowsSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
dSteps: DefineSpec('number'),
uMaxDistance: UniformSpec('f'),
uTolerance: UniformSpec('f'),
uBias: UniformSpec('f'),
uLightDirection: UniformSpec('v3[]'),
uLightColor: UniformSpec('v3[]'),
dLightCount: DefineSpec('number'),
};
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
const width = depthTexture.getWidth();
const height = depthTexture.getHeight();
const values: Values<typeof ShadowsSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
uTexSize: ValueCell.create(Vec2.create(width, height)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
dSteps: ValueCell.create(1),
uMaxDistance: ValueCell.create(3.0),
uTolerance: ValueCell.create(1.0),
uBias: ValueCell.create(0.6),
uLightDirection: ValueCell.create([]),
uLightColor: ValueCell.create([]),
dLightCount: ValueCell.create(0),
};
const schema = { ...ShadowsSchema };
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
@@ -84,14 +151,23 @@ const SsaoSchema = {
uRadius: UniformSpec('f'),
uBias: UniformSpec('f'),
dMultiScale: DefineSpec('boolean'),
dLevels: DefineSpec('number'),
uLevelRadius: UniformSpec('f[]'),
uLevelBias: UniformSpec('f[]'),
uNearThreshold: UniformSpec('f'),
uFarThreshold: UniformSpec('f'),
};
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRenderable {
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
const values: Values<typeof SsaoSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
tDepthHalf: ValueCell.create(depthHalfTexture),
tDepthQuarter: ValueCell.create(depthQuarterTexture),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),
@@ -102,8 +178,15 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
uRadius: ValueCell.create(8.0),
uBias: ValueCell.create(0.025),
uRadius: ValueCell.create(Math.pow(2, 5)),
uBias: ValueCell.create(0.8),
dMultiScale: ValueCell.create(false),
dLevels: ValueCell.create(3),
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
uNearThreshold: ValueCell.create(10.0),
uFarThreshold: ValueCell.create(1500.0),
};
const schema = { ...SsaoSchema };
@@ -124,8 +207,7 @@ const SsaoBlurSchema = {
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uMaxPossibleViewZDiff: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
@@ -146,8 +228,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
@@ -204,6 +285,7 @@ const PostprocessingSchema = {
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
@@ -214,26 +296,28 @@ const PostprocessingSchema = {
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uOutlineColor: UniformSpec('v3'),
uOcclusionColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('b'),
uMaxPossibleViewZDiff: UniformSpec('f'),
dOcclusionEnable: DefineSpec('boolean'),
uOcclusionOffset: UniformSpec('v2'),
dShadowEnable: DefineSpec('boolean'),
dOutlineEnable: DefineSpec('boolean'),
dOutlineScale: DefineSpec('number'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture): PostprocessingRenderable {
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
const values: Values<typeof PostprocessingSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
tColor: ValueCell.create(colorTexture),
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tShadows: ValueCell.create(shadowsTexture),
tOutlines: ValueCell.create(outlinesTexture),
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
@@ -244,16 +328,17 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
uTransparentBackground: ValueCell.create(false),
uMaxPossibleViewZDiff: ValueCell.create(0.5),
dOcclusionEnable: ValueCell.create(true),
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
dShadowEnable: ValueCell.create(false),
dOutlineEnable: ValueCell.create(false),
dOutlineScale: ValueCell.create(1),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...PostprocessingSchema };
@@ -267,18 +352,45 @@ export const PostprocessingParams = {
occlusion: PD.MappedStatic('on', {
on: PD.Group({
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
radius: PD.Numeric(5, { min: 0, max: 10, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
multiScale: PD.MappedStatic('off', {
on: PD.Group({
levels: PD.ObjectList({
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
{ radius: 2, bias: 1 },
{ radius: 5, bias: 1 },
{ radius: 8, bias: 1 },
{ radius: 11, bias: 1 },
] }),
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
}),
off: PD.Group({})
}, { cycle: true }),
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
}),
off: PD.Group({})
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
shadow: PD.MappedStatic('off', {
on: PD.Group({
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
}),
off: PD.Group({})
}, { cycle: true, description: 'Simplistic shadows' }),
outline: PD.MappedStatic('off', {
on: PD.Group({
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
color: PD.Color(Color(0x000000)),
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
}),
off: PD.Group({})
}, { cycle: true, description: 'Draw outline around 3D objects' }),
@@ -289,15 +401,37 @@ export const PostprocessingParams = {
}, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
background: PD.Group(BackgroundParams, { isFlat: true }),
};
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
type Levels = {
count: number
radius: number[]
bias: number[]
}
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
const count = props.length;
const { radius, bias } = levels || {
radius: (new Array(count * 3)).fill(0),
bias: (new Array(count * 3)).fill(0),
};
props = props.slice().sort((a, b) => a.radius - b.radius);
for (let i = 0; i < count; ++i) {
const p = props[i];
radius[i] = Math.pow(2, p.radius);
bias[i] = p.bias;
}
return { count, radius, bias };
}
export class PostprocessingPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
}
static isOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on';
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on' && props.outline.params.includeTransparent;
}
readonly target: RenderTarget;
@@ -305,6 +439,9 @@ export class PostprocessingPass {
private readonly outlinesTarget: RenderTarget;
private readonly outlinesRenderable: OutlinesRenderable;
private readonly shadowsTarget: RenderTarget;
private readonly shadowsRenderable: ShadowsRenderable;
private readonly ssaoFramebuffer: Framebuffer;
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
@@ -312,6 +449,12 @@ export class PostprocessingPass {
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
private readonly ssaoDepthTexture: Texture;
private readonly ssaoDepthBlurProxyTexture: Texture;
@@ -331,6 +474,8 @@ export class PostprocessingPass {
return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
}
private levels: { radius: number, bias: number }[];
private readonly bgColor = Vec3();
readonly background: BackgroundPass;
@@ -343,12 +488,16 @@ export class PostprocessingPass {
this.blurKernelSize = 1;
this.downsampleFactor = 1;
this.ssaoScale = this.calcSsaoScale();
this.levels = [];
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
this.ssaoFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
@@ -357,9 +506,27 @@ export class PostprocessingPass {
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.downsampledDepthTarget = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear');
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.downsampledDepthTarget = drawPass.packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', 'linear', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
@@ -370,10 +537,10 @@ export class PostprocessingPass {
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, this.downsampleFactor === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture);
this.ssaoRenderable = getSsaoRenderable(webgl, this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
this.background = new BackgroundPass(webgl, assetManager, width, height);
}
@@ -385,17 +552,30 @@ export class PostprocessingPass {
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.ssaoScale = ssaoScale;
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.target.setSize(width, height);
this.outlinesTarget.setSize(width, height);
this.shadowsTarget.setSize(width, height);
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.max(1, Math.floor(sw * 0.5));
const hh = Math.max(1, Math.floor(sh * 0.5));
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.max(1, Math.floor(sw * 0.25));
const qh = Math.max(1, Math.floor(sh * 0.25));
this.depthQuarterTarget.setSize(qw, qh);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
@@ -404,25 +584,29 @@ export class PostprocessingPass {
}
}
private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
private updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
let needsUpdateShadows = false;
let needsUpdateMain = false;
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateOutlines = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.outline.name === 'on';
const shadowsEnabled = props.shadow.name === 'on';
const occlusionEnabled = props.occlusion.name === 'on';
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const v = camera.viewport;
if (props.occlusion.name === 'on') {
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const b = this.ssaoRenderable.values.uBounds;
const v = camera.viewport;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
@@ -440,11 +624,14 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
if (this.nSamples !== props.occlusion.params.samples) {
needsUpdateSsao = true;
@@ -453,7 +640,30 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
const multiScale = props.occlusion.params.multiScale.name === 'on';
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
}
if (props.occlusion.params.multiScale.name === 'on') {
const mp = props.occlusion.params.multiScale.params;
if (!deepEqual(this.levels, mp.levels)) {
needsUpdateSsao = true;
this.levels = mp.levels;
const levels = getLevels(mp.levels);
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
} else {
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
@@ -464,8 +674,8 @@ export class PostprocessingPass {
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
if (this.downsampleFactor !== props.occlusion.params.resolutionScale) {
@@ -476,42 +686,101 @@ export class PostprocessingPass {
const sw = Math.floor(w * this.ssaoScale);
const sh = Math.floor(h * this.ssaoScale);
this.downsampledDepthTarget.setSize(sw, sh);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
const hw = Math.floor(sw * 0.5);
const hh = Math.floor(sh * 0.5);
this.depthHalfTarget.setSize(hw, hh);
const qw = Math.floor(sw * 0.25);
const qh = Math.floor(sh * 0.25);
this.depthQuarterTarget.setSize(qw, qh);
if (this.ssaoScale === 1) {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureTransparent);
ValueCell.update(this.ssaoRenderable.values.tDepth, this.drawPass.depthTextureOpaque);
} else {
ValueCell.update(this.ssaoRenderable.values.tDepth, this.downsampledDepthTarget.texture);
}
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
}
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
}
if (props.shadow.name === 'on') {
ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
needsUpdateShadows = true;
}
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
needsUpdateShadows = true;
}
ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
needsUpdateShadows = true;
}
}
if (props.outline.name === 'on') {
let { threshold } = props.outline.params;
// orthographic needs lower threshold
if (camera.state.mode === 'orthographic') threshold /= 5;
const factor = Math.pow(1000, threshold) / 1000;
// use radiusMax for stable outlines when zooming
const maxPossibleViewZDiff = factor * camera.state.radiusMax;
const transparentOutline = props.outline.params.includeTransparent ?? true;
const outlineScale = props.outline.params.scale - 1;
const outlineThreshold = 50 * props.outline.params.threshold;
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection);
if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
}
if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateOutlines = true;
ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
}
ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
ValueCell.updateIfChanged(this.renderable.values.uMaxPossibleViewZDiff, maxPossibleViewZDiff);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineScale, outlineScale);
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineScale, outlineScale);
}
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
}
}
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
@@ -520,12 +789,31 @@ export class PostprocessingPass {
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOutlineEnable, outlinesEnabled);
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) { needsUpdateMain = true; }
ValueCell.updateIfChanged(this.renderable.values.dOcclusionEnable, occlusionEnabled);
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
}
if (this.renderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOutlineEnable, outlinesEnabled);
}
if (this.renderable.values.dShadowEnable.ref.value !== shadowsEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dShadowEnable, shadowsEnabled);
}
if (this.renderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
}
if (needsUpdateShadows) {
this.shadowsRenderable.update();
}
if (needsUpdateSsao) {
this.ssaoRenderable.update();
@@ -546,10 +834,6 @@ export class PostprocessingPass {
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
const { x, y, width, height } = camera.viewport;
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
}
private occlusionOffset: [x: number, y: number] = [0, 0];
@@ -564,23 +848,42 @@ export class PostprocessingPass {
this.transparentBackground = value;
}
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
this.updateState(camera, transparentBackground, backgroundColor, props);
this.updateState(camera, transparentBackground, backgroundColor, props, light);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
const { gl, state } = this.webgl;
const { x, y, width, height } = camera.viewport;
// don't render occlusion if offset is given,
// which will reuse the existing occlusion
if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
const sh = Math.ceil(height * this.ssaoScale);
state.viewport(sx, sy, sw, sh);
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
this.ssaoFramebuffer.bind();
this.ssaoRenderable.render();
@@ -589,6 +892,20 @@ export class PostprocessingPass {
this.ssaoBlurSecondPassFramebuffer.bind();
this.ssaoBlurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
}
if (toDrawingBuffer) {
@@ -597,8 +914,6 @@ export class PostprocessingPass {
this.target.bind();
}
const { gl, state } = this.webgl;
this.background.update(camera, props.background);
if (this.background.isEnabled(props.background)) {
if (this.transparentBackground) {
@@ -684,4 +999,3 @@ export class AntialiasingPass {
}
}
}

View File

@@ -93,20 +93,35 @@ namespace Column {
return !!v && !!(v as Column<any>).schema && !!(v as Column<any>).value;
}
export const enum ValueKind {
// Value kinds are accessed very of often
// Using a const enum is an internal optimization and is defined separately to better support
// compiling with isolatedModules flag in 3rd party use-cases of Mol*.
export const enum ValueKinds {
/** Defined value (= 0) */
Present = 0,
/** Expressed in CIF as `.` */
/** Expressed in CIF as `.` (= 1) */
NotPresent = 1,
/** Expressed in CIF as `?` */
/** Expressed in CIF as `?` (= 2) */
Unknown = 2
}
export const ValueKind = {
/** Defined value (= 0) */
Present: ValueKinds.Present,
/** Expressed in CIF as `.` (= 1) */
NotPresent: ValueKinds.NotPresent,
/** Expressed in CIF as `?` (= 2) */
Unknown: ValueKinds.Unknown
} as const;
export type ValueKind = (typeof ValueKind)[keyof typeof ValueKinds];
export function Undefined<T extends Schema>(rowCount: number, schema: T): Column<T['T']> {
return constColumn(schema['T'], rowCount, schema, ValueKind.NotPresent);
return constColumn(schema['T'], rowCount, schema, ValueKinds.NotPresent);
}
export function ofConst<T extends Schema>(v: T['T'], rowCount: number, type: T): Column<T['T']> {
return constColumn(v, rowCount, type, ValueKind.Present);
return constColumn(v, rowCount, type, ValueKinds.Present);
}
export function ofLambda<T extends Schema>(spec: LambdaSpec<T>): Column<T['T']> {
@@ -256,7 +271,7 @@ function constColumn<T extends Column.Schema>(v: T['T'], rowCount: number, schem
return {
schema: schema,
__array: void 0,
isDefined: valueKind === Column.ValueKind.Present,
isDefined: valueKind === Column.ValueKinds.Present,
rowCount,
value,
valueKind: row => valueKind,
@@ -276,7 +291,7 @@ function lambdaColumn<T extends Column.Schema>({ value, valueKind, areValuesEqua
isDefined: true,
rowCount,
value,
valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
valueKind: valueKind ? valueKind : row => Column.ValueKinds.Present,
toArray: params => {
const { array, start } = ColumnHelpers.createArray(rowCount, params);
for (let i = 0, _i = array.length; i < _i; i++) array[i] = value(i + start);
@@ -304,7 +319,7 @@ function arrayColumn<T extends Column.Schema>({ array, schema, valueKind }: Colu
isDefined: true,
rowCount,
value,
valueKind: valueKind ? valueKind : row => Column.ValueKind.Present,
valueKind: valueKind ? valueKind : row => Column.ValueKinds.Present,
toArray: schema.valueType === 'str'
? (schema as Column.Schema.Str).transform === 'lowercase'
? params => {

View File

@@ -85,7 +85,7 @@ namespace Table {
rowCount,
schema: schema[k],
value: r => rows[r][k],
valueKind: r => typeof rows[r][k] === 'undefined' ? Column.ValueKind.NotPresent : Column.ValueKind.Present
valueKind: r => typeof rows[r][k] === 'undefined' ? Column.ValueKinds.NotPresent : Column.ValueKinds.Present
});
}
return ret as R;
@@ -267,7 +267,7 @@ namespace Table {
StringBuilder.write(sb, '|');
for (let i = 0; i < cols.length; i++) {
const c = table[cols[i]];
if (c.valueKind(r) === Column.ValueKind.Present) {
if (c.valueKind(r) === Column.ValueKinds.Present) {
StringBuilder.write(sb, c.value(r));
StringBuilder.write(sb, '|');
} else {

View File

@@ -93,7 +93,7 @@ export namespace BaseGeometry {
if (!transform) transform = createIdentityTransform();
const locationIterator = LocationIterator(1, transform.instanceCount.ref.value, 1, () => NullLocation, false, () => false);
const theme: Theme = {
color: UniformColorTheme({}, { value: colorValue }),
color: UniformColorTheme({}, { value: colorValue, lightness: 0, saturation: 0 }),
size: UniformSizeTheme({}, { value: sizeValue })
};
return { transform, locationIterator, theme };
@@ -132,8 +132,8 @@ export namespace BaseGeometry {
ValueCell.updateIfChanged(values.uBumpiness, props.material.bumpiness);
const clip = Clip.getClip(props.clip);
ValueCell.update(values.dClipObjectCount, clip.objects.count);
ValueCell.update(values.dClipVariant, clip.variant);
ValueCell.updateIfChanged(values.dClipObjectCount, clip.objects.count);
ValueCell.updateIfChanged(values.dClipVariant, clip.variant);
ValueCell.update(values.uClipObjectType, clip.objects.type);
ValueCell.update(values.uClipObjectInvert, clip.objects.invert);
ValueCell.update(values.uClipObjectPosition, clip.objects.position);

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2020 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>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { ChunkedArray } from '../../../mol-data/util';
@@ -10,7 +11,7 @@ import { Vec3 } from '../../../mol-math/linear-algebra';
export interface CylindersBuilder {
add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
addFixedCountDashes(start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number): void
addFixedLengthDashes(start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number): void
getCylinders(): Cylinders
}
@@ -41,19 +42,24 @@ export namespace CylindersBuilder {
}
};
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
const addFixedCountDashes = (start: Vec3, end: Vec3, segmentCount: number, radiusScale: number, topCap: boolean, bottomCap: boolean, stubCap: boolean, group: number) => {
const d = Vec3.distance(start, end);
const s = Math.floor(segmentCount / 2);
const step = 1 / segmentCount;
const isOdd = segmentCount % 2 !== 0;
const s = Math.floor((segmentCount + 1) / 2);
const step = d / (segmentCount + 0.5);
Vec3.sub(tmpDir, end, start);
Vec3.setMagnitude(tmpDir, Vec3.sub(tmpDir, end, start), step);
Vec3.copy(tmpVecA, start);
for (let j = 0; j < s; ++j) {
const f = step * (j * 2 + 1);
Vec3.setMagnitude(tmpDir, tmpDir, d * f);
Vec3.add(tmpVecA, start, tmpDir);
Vec3.setMagnitude(tmpDir, tmpDir, d * step * ((j + 1) * 2));
Vec3.add(tmpVecB, start, tmpDir);
Vec3.add(tmpVecA, tmpVecA, tmpDir);
if (isOdd && j === s - 1) {
Vec3.copy(tmpVecB, end);
if (!stubCap) bottomCap = false;
} else {
Vec3.add(tmpVecB, tmpVecA, tmpDir);
}
add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], radiusScale, topCap, bottomCap, group);
Vec3.add(tmpVecA, tmpVecA, tmpDir);
}
};
@@ -62,7 +68,7 @@ export namespace CylindersBuilder {
addFixedCountDashes,
addFixedLengthDashes: (start: Vec3, end: Vec3, segmentLength: number, radiusScale: number, topCap: boolean, bottomCap: boolean, group: number) => {
const d = Vec3.distance(start, end);
addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, group);
addFixedCountDashes(start, end, d / segmentLength, radiusScale, topCap, bottomCap, true, group);
},
getCylinders: () => {
const cylinderCount = groups.elementCount / 6;

Some files were not shown because too many files have changed in this diff Show More