Compare commits

...

191 Commits

Author SHA1 Message Date
Alexander Rose
2c10dd46a0 4.5.0 2024-07-28 17:34:12 -07:00
Alexander Rose
d4c80fc995 changelog 2024-07-28 17:30:50 -07:00
Alexander Rose
e1c00f65a5 package updates 2024-07-28 17:29:36 -07:00
Alexander Rose
012bc9e8e8 Merge pull request #1209 from molstar/fix-traj-color-update
ensure color-theme gets correctly updated
2024-07-28 17:27:00 -07:00
Alexander Rose
a99083107c Merge pull request #1207 from giagitom/separed-postprocessing-passes
Separed postprocessing passes
2024-07-28 17:23:15 -07:00
Alexander Rose
7e93bb0dda update filter in schema 2024-07-28 17:20:44 -07:00
giagitom
9735cce043 Merge branch 'master' of https://github.com/molstar/molstar into separed-postprocessing-passes 2024-07-28 12:53:06 +02:00
Alexander Rose
78e1d76f5e Merge branch 'master' into fix-traj-color-update 2024-07-25 22:14:54 -07:00
Alexander Rose
18b1492d54 schema update
- force missing auth_seq_id fields to be int
2024-07-25 22:13:10 -07:00
Alexander Rose
6116b2fea5 Merge pull request #1211 from molstar/ssao-filter
use texture linear filter for SSAO when available
2024-07-25 22:11:10 -07:00
Alexander Rose
6ef8fd2b64 handle missing theme updates
- Fix trajectory-index color-theme not always updated (#896)
 - Fix bond cylinders not updated on size-theme change with `adjustCylinderLength` enabled (#1215)
2024-07-25 22:08:39 -07:00
giagitom
9319805d36 Fix textue size not updating correctly 2024-07-24 11:58:21 +02:00
Alexander Rose
5027ad37d7 use texture linear filter for SSAO when available 2024-07-22 20:55:02 -07:00
Alexander Rose
70bd0c25c4 schema update
- add struct_mon_prot_cis mmcif category
2024-07-22 20:52:50 -07:00
Alexander Rose
1a5c7f5437 cleanup 2024-07-21 14:18:35 -07:00
Alexander Rose
4a9505c334 Merge branch 'master' of https://github.com/molstar/molstar into fix-traj-color-update 2024-07-20 13:40:41 -07:00
Alexander Rose
b43ec9ed45 Merge branch 'master' of https://github.com/molstar/molstar 2024-07-20 13:37:46 -07:00
Alexander Rose
eb9c6d542b package updates 2024-07-20 13:37:44 -07:00
Alexander Rose
2ec0911821 Merge pull request #1201 from rjdirisio/further-sass-syntax-fixes
Continue fixing syntax for Sass deprecation of special semantics
2024-07-20 13:31:28 -07:00
Alexander Rose
bbb34c8a27 ensure color-theme gets correctly updated
- fixes trajectory-index color-theme (#896)
2024-07-20 13:30:26 -07:00
giagitom
1bcb8d6486 Merge remote-tracking branch 'upstream/master' into separed-postprocessing-passes 2024-07-17 16:29:31 +02:00
giagitom
630b5ca203 Separated postprocessing passes 2024-07-17 16:16:53 +02:00
Alexander Rose
5b2ed784e1 Merge pull request #1204 from corredD/patch-1
Update states.tsx docs url
2024-07-16 21:02:18 -07:00
Alexander Rose
11c9a83ee7 Merge pull request #1205 from corredD/ME_fix_loadUrl
Load URL now behave like drag/drop.
2024-07-16 21:00:39 -07:00
ludovic autin
2d1b61647a Load URL now behave like drag/drop. Fix some error loading state snapshot tours 2024-07-16 11:46:25 -07:00
ludovic autin
fa3797a738 Update states.tsx docs url 2024-07-16 10:40:43 -07:00
Alexander Rose
fc60c0c980 Merge pull request #1191 from bergwerf/aromatic_links
Add unadjusted position for aromatic link dashes
2024-07-14 10:42:34 -07:00
Alexander Rose
372ca20980 simplify render-structure-grid test 2024-07-14 10:35:35 -07:00
Alexander Rose
b0aad9f1ff add missing adjust arg 2024-07-14 10:15:35 -07:00
Alexander Rose
40e45adbb0 Merge branch 'master' of https://github.com/molstar/molstar into pr/bergwerf/1191 2024-07-14 10:05:40 -07:00
Alexander Rose
5b43a2cee9 me ui header tweaks 2024-07-14 10:03:10 -07:00
Alexander Rose
a319a0daa8 update me deploy path 2024-07-13 18:44:19 -07:00
Alexander Rose
79f812d0e1 update me deploy path 2024-07-13 18:01:34 -07:00
Ryan DiRisio
45611a25a5 Continue fixing syntax 2024-07-12 12:25:57 -04:00
Ryan DiRisio
77be659915 Update misc.scss msp-no-webgl syntax to be in line with latest sass requirements (#1199)
* wrap args in ampersand for msp-no-webgl

* update changelog

---------

Co-authored-by: Ryan DiRisio <rdirisio@treeline.bio>
2024-07-12 16:54:40 +02:00
Herman Bergwerf
5986250ed9 Make Trackball spin in radians per second (#1193)
* Make Trackball spin in radians per second.

* Add line to changelog.
2024-07-08 18:03:02 +02:00
Herman Bergwerf
8156c672b0 Fix formatting. 2024-07-08 15:59:59 +02:00
Herman Bergwerf
a443512102 Merge branch 'master' into aromatic_links 2024-07-08 15:52:14 +02:00
Herman Bergwerf
4b921319a8 Improve pinch event (#1192)
* Improve pinch event.

* Apply feedback.

* Update changelog and headers.

* Add me to contributers.
2024-07-08 15:39:00 +02:00
Herman Bergwerf
6329820a87 Add line to changelog. 2024-07-08 15:14:26 +02:00
Herman Bergwerf
91e4b0c3d6 Add me to file headers. 2024-07-08 15:11:53 +02:00
Herman Bergwerf
7666617857 Alter LinkBuilderProps.position signature. 2024-07-08 15:10:04 +02:00
Herman Bergwerf
19be1090b3 Add unadjusted position for aromatic link dashes. 2024-07-07 16:39:38 +02:00
Alexander Rose
354438052e Merge pull request #1176 from papillot/hbonds_with_explicit_hydrogens
Take into account explicit hydrogens when creating and representing Hydrogen bonds (regular and weak)
2024-07-06 12:03:05 -07:00
Alexander Rose
b72444b213 Merge branch 'master' of https://github.com/molstar/molstar into pr/papillot/1176 2024-07-06 11:55:51 -07:00
Alexander Rose
179078f45c remove unused postprocessing code (#1189) 2024-07-06 11:48:53 -07:00
Alexander Rose
1dbc23fe91 dof fixes
- handle pixel ratios =! 1
- ensure transparent depths is available
2024-07-06 11:29:31 -07:00
Paul Pillot
ff4dec9fea code styling, typos 2024-07-01 09:43:10 +02:00
Paul Pillot
6ea51c07b4 update changelog, author information 2024-07-01 09:32:11 +02:00
Paul Pillot
f4cebb9195 manage weak hydrogen bonds position
Previous code was filtering on `polar` hydrogens when looking for an anchorage.
This was not necessary as an acceptor for a regular HBond has only polar hydrogens.
It was removed to make the same code compliant with weak C-H bonds.
2024-07-01 09:27:46 +02:00
Paul Pillot
cbe5f0dc7c Add ignoreHydrogens parameter to geometry options
This is different from the "Hydrogens: Show All/Hide All/Only Polar" as it is used for filtering out which contacts are represented, while the latter is used to control the display (point to the heavy atoms when H are hidden).
In other words, by considering the H atoms in the geometric constraints, some candidate H bonds are discarded (e.g. because the donor does not have H atoms in the right orientation), while the visibility of hydrogens parameters will still show the same number of hydrogen bonds, just not pointing to the same atoms.
2024-07-01 09:27:46 +02:00
Paul Pillot
0ac8b565b5 Use explicit hydrogens to calculate angles
Donor: at least one hydrogen on the donor must be within the donor angle deviation towards the acceptor (angle H-X-A < 45º)

Acceptor: use the closest H on the donor to compute the angles. Ensure that every H atom on the acceptor complies with the ideal angle (no bonded atom from acceptor should be in the way).
2024-07-01 09:27:46 +02:00
Paul Pillot
4f38d4d943 relocate hbond on hydrogens from donors only
This avoids hbonds between 2 hydrogen atoms
2024-07-01 09:27:46 +02:00
Alexander Rose
0af84eb6b5 4.4.1 2024-06-30 15:53:58 -07:00
Alexander Rose
aa2d19478b changelog 2024-06-30 15:51:28 -07:00
Alexander Rose
e035b834a6 update package-lock 2024-06-30 15:43:34 -07:00
Alexander Rose
2b2dfd9245 4.4.0 2024-06-30 15:35:36 -07:00
Alexander Rose
678790efa3 changelog 2024-06-30 15:32:59 -07:00
Alexander Rose
a121c5e2cd fix missing unit.kind check 2024-06-30 13:31:09 -07:00
Alexander Rose
47b242244e schema updates 2024-06-30 13:25:30 -07:00
Alexander Rose
2b9d3fd33a packge updates 2024-06-30 13:25:18 -07:00
Alexander Rose
81404036a2 Merge pull request #1182 from corredD/driver_tour
Mesoscale Explorer Driver tour
2024-06-30 13:15:40 -07:00
Alexander Rose
e2dc15cf0f cleanup, fixes & changelog 2024-06-30 13:14:58 -07:00
Ludovic Autin
365a91879f clean up 2024-06-30 10:10:06 -07:00
Ludovic Autin
e47e0eb51a Merge branch 'master' into driver_tour 2024-06-30 10:02:31 -07:00
Alexander Rose
390046e38f Merge pull request #1184 from JonStargaryen/master
Remove support for density data from edmaps.rcsb.org
2024-06-29 22:27:01 -07:00
Sebastian Bittrich
b8eb5191a2 rm support for edmaps.rcsb.org, closes #1127 2024-06-28 11:16:53 -07:00
ludovic autin
4cc416ca28 driver is in the extra folder of molstar.github.io 2024-06-27 11:38:30 -07:00
ludovic autin
f4b2458390 Merge branch 'master' into driver_tour 2024-06-27 10:02:39 -07:00
David Sehnal
1cad6eef74 doNotDisposeCanvas3DContext option in PluginContext.dispose (#1180)
* doNotDisposeCanvas3DContext option in PluginContext

* header
2024-06-27 15:45:05 +02:00
Alexander Rose
2923be6006 Merge pull request #1175 from corredD/list_tours
file holdng the molx available tour.
2024-06-26 21:26:14 -07:00
ludovic autin
70959641a1 Merge branch 'master' into driver_tour
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
#	src/apps/mesoscale-explorer/ui/states.tsx
2024-06-26 10:35:43 -07:00
ludovic autin
051608f56c file holdng the molx available tour. 2024-06-24 13:13:32 -07:00
Alexander Rose
71c1a4e85b Merge pull request #1167 from corredD/ME_quick_style
Mesoscale Explorer Quick Style
2024-06-23 22:30:13 -07:00
Alexander Rose
ba06c9e413 changelog 2024-06-23 22:29:23 -07:00
Ludovic Autin
8e4dfd1ffd default collapsed for structure measurement like the quickstyle. 2024-06-23 17:59:53 -07:00
Ludovic Autin
fac8aa529f spelling + shiny style 2024-06-23 17:51:57 -07:00
Ludovic Autin
d35b4b5e62 Merge branch 'master' into ME_quick_style 2024-06-23 17:13:38 -07:00
Alexander Rose
dd1789478b Merge pull request #1161 from corredD/me_snapshot_descrption
Mesoscale Explorer Snapshot Description
2024-06-23 17:01:33 -07:00
Alexander Rose
af27a00a01 changelog 2024-06-23 17:00:55 -07:00
Ludovic Autin
17cea8f99c not sure why this changes was not in the merge. 2024-06-23 16:14:14 -07:00
Ludovic Autin
4f6d5a7dc7 Merge branch 'master' into me_snapshot_descrption
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
use custom snapshotDescription
2024-06-23 16:08:23 -07:00
ludovic autin
857972653e Merge branch 'molstar:master' into ME_quick_style 2024-06-23 15:47:32 -07:00
Alexander Rose
98ff0f5c55 Merge pull request #1169 from molstar/customize-snapshot-desc
Ability to customize snapshot description component
2024-06-23 15:20:13 -07:00
Alexander Rose
43803a91ea Merge pull request #1170 from molstar/cel-shading-artifacts
cel-shading artifacts
2024-06-23 15:19:13 -07:00
Alexander Rose
8a1bab8bcb Merge pull request #1171 from giagitom/clean_solidInterior_transparent_cylinders
Clean solid interior transparent cylinders
2024-06-23 15:18:55 -07:00
Ludovic Autin
17a47faaff Merge branch 'master' into ME_quick_style2
# Conflicts:
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-23 15:10:14 -07:00
Ludovic Autin
f793167e91 cleanup, fix cel-shading spelling 2024-06-23 15:06:16 -07:00
giagitom
d1c2c8e837 Updated headers and changelog 2024-06-23 23:42:27 +02:00
giagitom
5d7ef8196e Merge branch 'master' of https://github.com/molstar/molstar into clean_solidInterior_transparent_cylinders 2024-06-23 23:39:21 +02:00
giagitom
e0715cbf5c clean solidInterior transparent cylinders 2024-06-23 23:33:11 +02:00
Alexander Rose
1af8522de3 cel-shading artifacts
- clamp metalness & roughness to look better
2024-06-23 14:30:53 -07:00
Alexander Rose
c6becd5741 Merge pull request #1166 from corredD/ME_select_behavior
Mesoscale Explorer mouse select behavior
2024-06-23 14:06:58 -07:00
Alexander Rose
0ca368f29f changelog 2024-06-23 14:06:45 -07:00
Alexander Rose
5039a448ad cleanup 2024-06-23 13:44:23 -07:00
Ludovic Autin
9ef38f02c9 code fix, and make sure style stay when changing color scheme in the tree. 2024-06-23 10:41:00 -07:00
dsehnal
66f4ff1140 fix types 2024-06-23 18:48:25 +02:00
dsehnal
cc077656a9 ability to customize snapshot description component 2024-06-23 18:30:31 +02:00
Ludovic Autin
3ef1a2ec0a remove gamma, and use exposure instead. Expose renderer/trackball advance settings in left panel.
only expose couple of quick-style.
2024-06-22 13:04:31 -07:00
ludovic autin
144bf6954e apply gamma only if > 1 2024-06-21 11:39:40 -07:00
ludovic autin
50e5538148 - Fixed shadow fallof
- Add Gamma effect
- Quick Style UI default / illustrative cell shading / DOF plastic
2024-06-21 11:32:13 -07:00
ludovic autin
efe95f92c7 also added the StructureMeasurementsControls in the right panel below the selection. 2024-06-21 10:16:06 -07:00
ludovic autin
09f858a755 mouse left click on label with snapsot key load the snapshot. mouse hover label with protein name highlight entities with the same name 2024-06-21 09:48:51 -07:00
ludovic autin
e7082d4ccc focusinfo and other ID 2024-06-19 10:44:19 -07:00
ludovic autin
732a8f4bd0 Merge branch 'master' into driver_tour
# Conflicts:
#	src/apps/mesoscale-explorer/ui/entities.tsx
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-19 10:00:32 -07:00
ludovic autin
82ca06b29e use MesoMarkdownAnchor for the focusInfo 2024-06-19 09:26:01 -07:00
ludovic autin
a05429f13f Merge branch 'master' into me_snapshot_descrption
# Conflicts:
#	src/apps/mesoscale-explorer/data/state.ts
#	src/apps/mesoscale-explorer/ui/entities.tsx
#	src/apps/mesoscale-explorer/ui/panels.tsx
2024-06-19 09:21:29 -07:00
Alexander Rose
377de7ad40 Merge pull request #1118 from giagitom/quantized-light
Add quantized lighting option
2024-06-18 21:50:48 -07:00
Alexander Rose
74f4d00c8d changelog 2024-06-18 21:50:21 -07:00
Alexander Rose
be3825372e lint fix 2024-06-18 21:47:12 -07:00
Alexander Rose
d62c5c9050 Merge pull request #1157 from corredD/me_selection_description
Mesoscale explorer entities selection description
2024-06-18 21:45:21 -07:00
ludovic autin
9ba5112beb revert base css class, create the custom css class in me app style.css .msp-snapshot-description-me 2024-06-17 10:15:16 -07:00
ludovic autin
048658ee39 min,max text size 2024-06-17 09:50:42 -07:00
ludovic autin
2918081dd9 Merge branch 'master' into me_selection_description 2024-06-17 09:43:03 -07:00
ludovic autin
1a67868c07 Merge branch 'master' into me_snapshot_descrption 2024-06-17 09:42:35 -07:00
Alexander Rose
525dfaddd2 add cel-shading
- celShaded geometry parameter
- celSteps renderer parameter
2024-06-16 12:45:06 -07:00
Alexander Rose
8aa12c0d31 Merge branch 'master' of https://github.com/molstar/molstar into pr/giagitom/1118 2024-06-15 14:25:47 -07:00
Alexander Rose
8cb464a686 Merge pull request #1159 from molstar/plddt-bfactor
support reading score from B-factor in pLDDT color theme
2024-06-15 14:24:09 -07:00
Alexander Rose
e0371d7e32 Update src/extensions/model-archive/quality-assessment/color/plddt.ts
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 14:22:43 -07:00
Ludovic Autin
e7da6bc194 duplicate preventDefault, rename function 2024-06-15 10:32:14 -07:00
ludovic autin
933869b5e1 Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:21:19 -07:00
Ludovic Autin
5d6adc46fe simplify get info(). fix import 2024-06-15 10:08:00 -07:00
ludovic autin
b8a98efcaf Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:58 -07:00
ludovic autin
53b358f70a Update src/apps/mesoscale-explorer/ui/entities.tsx
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:34 -07:00
ludovic autin
bce9e5b0ad Update src/apps/mesoscale-explorer/data/mmcif/model.ts
Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-15 10:03:06 -07:00
ludovic autin
cf7f9d6aba lint 2024-06-14 11:05:02 -07:00
ludovic autin
6af1bc5def snapshot description customized component to support more advanced markdown command for highlighting and selection 2024-06-14 10:57:00 -07:00
ludovic autin
936808e271 using driver.js 1.3.1 design an introductory tour of the user interface. 2024-06-14 10:30:54 -07:00
ludovic autin
8b985d0424 rename the descriptino holder; separate from the selection UIComponent and create its own component. Change its position to below the tree. 2024-06-14 09:40:05 -07:00
ludovic autin
b290cf121a Merge branch 'master' into me_selection_description 2024-06-14 08:57:30 -07:00
Alexander Rose
89df6cec42 support reading score from B-factor in pLDDT color theme 2024-06-13 23:28:14 -07:00
Alexander Rose
fc5fc7fcdb Merge pull request #1134 from molstar/occlusion-fixes
fixes for SSAO edge artifacts
2024-06-13 23:22:17 -07:00
Alexander Rose
a1de5bb304 Merge branch 'master' of https://github.com/molstar/molstar into occlusion-fixes 2024-06-13 23:17:14 -07:00
Alexander Rose
25c8a41e91 selection description tweaks 2024-06-13 22:54:34 -07:00
ludovic autin
959249b572 added getCellDescription to generate the markdown for the given cell. For mmCIF files use the entity row info to genereate a description. 2024-06-13 11:16:55 -07:00
Alexander Rose
44610b8b1a update cif schema 2024-06-12 21:50:49 -07:00
Alexander Rose
4070453209 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1157 2024-06-12 21:48:10 -07:00
Alexander Rose
cf2193f4fc Merge pull request #1156 from corredD/me_camera_behavior
mesoscale explorer centerOnly behavior
2024-06-12 21:41:46 -07:00
Alexander Rose
bfc0a3d1fe changelog 2024-06-12 21:41:28 -07:00
Alexander Rose
53a2155d8c Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1156 2024-06-12 21:40:04 -07:00
Alexander Rose
f22121521b Merge pull request #1152 from corredD/me_illustrative
enable illustrative color scheme in the tree
2024-06-12 19:52:20 -07:00
Alexander Rose
755655d067 add illustrative coloring option 2024-06-12 19:45:53 -07:00
ludovic autin
41ab186fd2 ME - entities can have description text retriev in the file in the entity field or in the manifest. The descripion appear for the current selection in the left panel and in the tips when mouse hove in the viewport. The markdown will use the same handling as the Snapshot description defined in the me_ui_label PR. 2024-06-12 12:37:35 -07:00
ludovic autin
9039c653cb centerOnly toggle is actually used. Its is exposed as a Key toggle 'c'. How can we expose all the MesoFocusLociProps in the UI ? So that user can easily toggle the centerOnly and change the transition duration... 2024-06-12 11:00:44 -07:00
ludovic autin
dde3f4ecff Merge branch 'master' into me_illustrative 2024-06-12 09:39:31 -07:00
ludovic autin
64cd05cc14 hide toggle in custom type 2024-06-11 10:24:18 -07:00
David Sehnal
593e8f4993 Improve camera interpolation (#1153)
* improve camera interpolation

* typos
2024-06-11 14:19:19 +02:00
ludovic autin
d2d9eb622f preset illustrative: false, 2024-06-10 10:11:27 -07:00
ludovic autin
ce9883517f illustrative scheme as a boolean. 2024-06-10 09:56:15 -07:00
Ludovic Autin
bc2afe1d68 Merge commit '5a2ee03b48e1822c8843d65eb18d578af0579b9f' into me_illustrative 2024-06-08 09:09:23 -07:00
Alexander Rose
5a2ee03b48 lint fix 2024-06-07 22:38:26 -07:00
ludovic autin
2d86c76788 no multispace 2024-06-07 16:56:51 -07:00
ludovic autin
7f8995a4d8 " 2024-06-07 16:50:01 -07:00
ludovic autin
64ab8bf78d enable illustrative color scheme in the tree 2024-06-07 16:44:29 -07:00
Alexander Rose
bf9663e177 handle near clip in ssao-blur 2024-06-06 23:09:02 -07:00
David Sehnal
fb729446e2 Adjust quick styles panel (#1143)
* adjust quick styles panel

* header

* Update src/mol-plugin-ui/structure/quick-styles.tsx

Co-authored-by: Paul Pillot <116235895+papillot@users.noreply.github.com>

---------

Co-authored-by: Paul Pillot <116235895+papillot@users.noreply.github.com>
2024-06-05 08:56:37 +02:00
Neli Fonseca
3e4082bf6e Decompress gzip volume (#1146)
* Created DecompressGzip transformer to render map.gz volumes correctly

* Updated changelog

* Update src/apps/viewer/app.ts

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

* Update src/apps/viewer/app.ts

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

* Update src/mol-plugin-state/transforms/data.ts

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

* Fixes bug and updated changelog

* Update src/apps/viewer/app.ts

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

* Update src/apps/viewer/app.ts

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

* Update src/apps/viewer/app.ts

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

* Update src/apps/viewer/app.ts

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

* Handle deflateData into string

* Update src/apps/viewer/app.ts

* Update src/mol-plugin-state/transforms/data.ts

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-06-04 12:14:49 +02:00
dsehnal
5af6a3e967 4.3.0 2024-05-26 09:18:19 +02:00
dsehnal
e718835042 lint ignore 2024-05-26 09:15:18 +02:00
dsehnal
70f0804e26 changelog 2024-05-26 09:07:40 +02:00
ludovic autin
e613a90754 await the the snapshot setting to avoid rendering while applying snap… (#1140)
* await the the snapshot setting to avoid rendering while applying snapshot. Request : export animation should take in account the trackball controller of 'rock' or 'spin' as well.

* add the updateController option when renderiing snapshot, camera rock and spin are thus taking into account

* forgot a file

* update the canvas3d using the current snapshot to allow camera rock and spin animation rendering

* changelog
2024-05-22 19:29:58 +02:00
David Sehnal
dca6affc84 fix RMSD minification bug (#1138)
* fix RMSD minification bug

* changelog
2024-05-21 14:23:21 +02:00
Alexander Rose
af33516107 fix date 2024-05-16 23:49:26 -07:00
Alexander Rose
7148c7197b fixes for SSAO edge artifacts
- add `reuseOcclusion` parameter to multi-sample pass
- add `blurBias` parameter to occlusion pass
2024-05-16 23:48:57 -07:00
Gianluca Tomasello
0d9d173ef4 Merge branch 'master' into quantized-light 2024-05-13 21:02:21 +02:00
giagitom
6eaf8e1911 Adding tweaks 2024-05-13 20:59:42 +02:00
Alexander Rose
b11e24cd06 Merge pull request #1103 from corredD/dof
DOF shader
2024-05-12 17:31:46 -07:00
Alexander Rose
1a1bce8193 Merge branch 'master' into dof 2024-05-12 17:25:57 -07:00
Alexander Rose
b67f7271fc changelog & docs 2024-05-11 15:12:06 -07:00
Alexander Rose
837b838766 cleanup DofPass.update & fix webgl1 support 2024-05-11 15:04:42 -07:00
Alexander Rose
7dd42421e2 changelog 2024-05-11 14:50:47 -07:00
Alexander Rose
258dc637fc fix input/output & support transparency 2024-05-11 14:49:27 -07:00
Alexander Rose
d6c594395c make 'camera-target' the default 2024-05-11 14:49:22 -07:00
Alexander Rose
746173fe33 undo changes to shadow shader 2024-05-11 14:47:45 -07:00
Alexander Rose
e9de12e6a2 improve Timer.clear 2024-05-11 13:33:27 -07:00
ludovic autin
3bfebceaea Merge branch 'molstar:master' into dof 2024-05-10 09:37:59 -07:00
Dušan Veľký
443fc9c60c Tunnels extension (#1105)
* create algorithm

* create props

* create representation

* add examples

* add providers to index

* updated tunnel props

* updated tunnel transformers

* add interpolation and sampleRate

* rename tunnel example

* update examples

* add headers

* add author to contributors

* fix runVisualizeTunnel example

* add docs for tunnels extension

* create action to download tunnels

* add download tunnels to viewer

* fix description for shape provider

* update docs

* rename props to data-model

* rename transformers

* add headers

* add url to PluginConfig

* fix ChannelsDB type

* reimplement interpolation

* fix geometry condition

* remove fillFactor

* rename projectTunnel to index

* fix samplingRate

* remove fillFactor

* rename loci to highlight_label

* fix functions args

* create preset for structure tunnels

* rename ids

* change preset to show all tunnels

---------

Co-authored-by: Dušan Veľký <velky.dusko@gmail.com>
2024-05-08 15:37:59 +02:00
giagitom
5c882f1aa5 Added author in headers 2024-05-06 16:19:43 +02:00
giagitom
2320518b87 Add quantized lighting option 2024-05-06 14:58:42 +02:00
Ludovic Autin
2124bead5e change header date and authors. Reduce the blurring at the edge of the inFocus area. 2024-05-05 16:47:47 -07:00
Ludovic Autin
a6077c7263 Merge branch 'master' into dof
# Conflicts:
#	src/mol-canvas3d/passes/draw.ts
#	src/mol-canvas3d/passes/postprocessing.ts
2024-05-05 10:12:31 -07:00
ludovic autin
6f478a3eb3 change default PPM, use colorTarget when no postprocessing is apply. 2024-05-01 09:31:53 -07:00
ludovic autin
a36c2feee4 clean up, pass center position transformed in view space 2024-05-01 08:37:22 -07:00
Ludovic Autin
a5e2946aa6 use scene center or camera target center, and blur in a plan parrallel to depth or in a sphere arround the center. 2024-05-01 00:03:57 -07:00
ludovic autin
00428254a8 work in progress using the camera target as the center of focus. 2024-04-30 14:15:58 -07:00
ludovic autin
53a57530c5 Merge branch 'master' into dof 2024-04-30 13:53:12 -07:00
ludovic autin
5ee6bee130 use the distance to the visible boundingsphere center to specifiy the area in focus 2024-04-29 10:26:11 -07:00
ludovic autin
734b6001c2 Merge branch 'master' into dof 2024-04-29 09:15:09 -07:00
ludovic autin
e88c2df42f add citation url 2024-04-24 15:39:35 -07:00
ludovic autin
79e6a4c95d dof files 2024-04-24 15:27:59 -07:00
ludovic autin
2f56b9c491 DOF shader 2024-04-24 12:27:58 -07:00
120 changed files with 5086 additions and 1524 deletions

View File

@@ -1,3 +1,4 @@
node_modules/*
build/*
docs/site/*
lib/*

View File

@@ -5,7 +5,64 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
## [v4.2.0] - 2023-04-05
## [v4.5.0] - 2023-07-28
- Separated postprocessing passes
- Take into account explicit hydrogens when computing hydrogen bonds
- Fix DoF with pixel ratios =! 1
- Fix DoF missing transparent depth
- Fix trackball pinch zoom and add pan
- Fix aromatic link rendering when `adjustCylinderLength` is true
- Change trackball animate spin speed unit to radians per second
- Fix `mol-plugin-ui/skin/base/components/misc.scss` syntax to be in line with latest Sass syntax
- Handle missing theme updates
- Fix trajectory-index color-theme not always updated (#896)
- Fix bond cylinders not updated on size-theme change with `adjustCylinderLength` enabled (#1215)
- Use `OES_texture_float_linear` for SSAO when available
## [v4.4.1] - 2023-06-30
- Clean `solidInterior` transparent cylinders
- Create a transformer to deflate compressed data
- Adjust Quick Styles panel button labels
- Improve camera interpolation code (interpolate camera rotation instead of just position)
- Mesoscale Explorer
- Add `illustrative` coloring option
- Press 'C' to toggle between center and zoom & center on click
- Add entities selection description
- Clicking a leaf node in the right panel tree will center each instance in turn
- Add measurement controls to right panel
- Mouse left click on label with snapshot key will load snapshot
- Mouse hover over label with protein name highlight entities with the same name
- Custom ViewportSnapshotDescription with custom MarkdowAnchor
- \# other snapshots with a given key \[...](#key)
- i highlight a protein with a given NAME \[...](iNAME)
- g highlight a group with a given group type and group name \[...](ggrouptype.groupname)
- h URLs with a given link \[...](http...)
- Snapshot description panel window size and text can be resized and hidden with new icons
- Add styles controls to right panel
- Add viewport settings to left panel
- Add app info component to left panel with interactive tour and doc link
- Fixes SSAO edge artifacts (#1122)
- Add `reuseOcclusion` parameter to multi-sample pass
- Add `blurDepthBias` parameter to occlusion pass
- Handle near clip in SSAO blur
- Support reading score from B-factor in pLDDT color theme
- Add Cel-shading support
- `celShaded` geometry parameter
- `celSteps` renderer parameter
- Add the ability to customize the Snapshot Description component via `PluginUISpec.components.viewport.snapshotDescription`
- Add `doNotDisposeCanvas3DContext` option to `PluginContext.dispose`
- Remove support for density data from edmaps.rcsb.org
## [v4.3.0] - 2023-05-26
- Fix State Snapshots export animation (#1140)
- Add depth of field (dof) postprocessing effect
- Add `SbNcbrTunnels` extension for for visualizing tunnels in molecular structures from ChannelsDB (more info in [tunnels.md](./docs/docs/extensions/tunnels.md))
- Fix edge case in minimizing RMSD transform computation
## [v4.2.0] - 2023-05-04
- Add emissive material support
- Add bloom post-processing

View File

@@ -107,6 +107,7 @@ entity.id
entity.type
entity.src_method
entity.pdbx_description
entity.pdbx_parent_entity_id
entity.formula_weight
entity.pdbx_number_of_molecules
entity.details
@@ -366,18 +367,43 @@ struct_site.details
struct_site_gen.id
struct_site_gen.site_id
struct_site_gen.pdbx_num_res
struct_site_gen.label_comp_id
struct_site_gen.auth_asym_id
struct_site_gen.auth_atom_id
struct_site_gen.auth_comp_id
struct_site_gen.auth_seq_id
struct_site_gen.details
struct_site_gen.label_alt_id
struct_site_gen.label_asym_id
struct_site_gen.label_atom_id
struct_site_gen.label_comp_id
struct_site_gen.label_seq_id
struct_site_gen.pdbx_auth_ins_code
struct_site_gen.auth_comp_id
struct_site_gen.auth_asym_id
struct_site_gen.auth_seq_id
struct_site_gen.label_atom_id
struct_site_gen.label_alt_id
struct_site_gen.pdbx_num_res
struct_site_gen.symmetry
struct_site_gen.details
struct_site_keywords.site_id
struct_site_keywords.text
struct_mon_prot_cis.pdbx_id
struct_mon_prot_cis.auth_asym_id
struct_mon_prot_cis.auth_comp_id
struct_mon_prot_cis.auth_seq_id
struct_mon_prot_cis.label_alt_id
struct_mon_prot_cis.label_asym_id
struct_mon_prot_cis.label_comp_id
struct_mon_prot_cis.label_seq_id
struct_mon_prot_cis.pdbx_PDB_ins_code
struct_mon_prot_cis.pdbx_PDB_ins_code_2
struct_mon_prot_cis.pdbx_PDB_model_num
struct_mon_prot_cis.pdbx_auth_asym_id_2
struct_mon_prot_cis.pdbx_auth_comp_id_2
struct_mon_prot_cis.pdbx_auth_ins_code
struct_mon_prot_cis.pdbx_auth_ins_code_2
struct_mon_prot_cis.pdbx_auth_seq_id_2
struct_mon_prot_cis.pdbx_label_asym_id_2
struct_mon_prot_cis.pdbx_label_comp_id_2
struct_mon_prot_cis.pdbx_label_seq_id_2
struct_mon_prot_cis.pdbx_omega_angle
symmetry.entry_id
symmetry.cell_setting
1 atom_sites.entry_id
107 entity_poly.nstd_linkage entity_poly.type
108 entity_poly.nstd_monomer entity_poly.nstd_linkage
109 entity_poly.pdbx_seq_one_letter_code entity_poly.nstd_monomer
110 entity_poly.pdbx_seq_one_letter_code
111 entity_poly.pdbx_seq_one_letter_code_can
112 entity_poly.pdbx_strand_id
113 entity_poly.pdbx_target_identifier
367 pdbx_reference_entity_link.details symmetry.space_group_name_H-M
368 pdbx_reference_entity_list.prd_id pdbx_molecule.instance_id
369 pdbx_reference_entity_list.ref_entity_id pdbx_molecule.prd_id
370 pdbx_reference_entity_list.component_id pdbx_molecule.asym_id
371 pdbx_reference_entity_list.type pdbx_molecule_features.prd_id
372 pdbx_molecule_features.name
373 pdbx_molecule_features.type
374 pdbx_molecule_features.class
375 pdbx_molecule_features.details
376 pdbx_reference_entity_list.details pdbx_reference_entity_link.prd_id
377 pdbx_reference_entity_link.link_id
378 pdbx_reference_entity_link.link_class
379 pdbx_reference_entity_poly_link.prd_id pdbx_reference_entity_link.ref_entity_id_1
380 pdbx_reference_entity_poly_link.ref_entity_id pdbx_reference_entity_link.entity_seq_num_1
381 pdbx_reference_entity_poly_link.link_id pdbx_reference_entity_link.comp_id_1
pdbx_reference_entity_poly_link.atom_id_1
pdbx_reference_entity_poly_link.comp_id_1
pdbx_reference_entity_poly_link.entity_seq_num_1
pdbx_reference_entity_poly_link.atom_id_2
382 pdbx_reference_entity_poly_link.comp_id_2 pdbx_reference_entity_link.atom_id_1
383 pdbx_reference_entity_poly_link.entity_seq_num_2 pdbx_reference_entity_link.ref_entity_id_2
384 pdbx_reference_entity_link.entity_seq_num_2
385 pdbx_reference_entity_link.comp_id_2
386 pdbx_reference_entity_link.atom_id_2
387 pdbx_reference_entity_link.value_order
388 pdbx_reference_entity_link.component_1
389 pdbx_reference_entity_link.component_2
390 pdbx_reference_entity_link.details
391 pdbx_reference_entity_list.prd_id
392 pdbx_reference_entity_list.ref_entity_id
393 pdbx_reference_entity_list.component_id
394 pdbx_reference_entity_list.type
395 pdbx_reference_entity_list.details
396 pdbx_reference_entity_poly_link.prd_id
397 pdbx_reference_entity_poly_link.ref_entity_id
398 pdbx_reference_entity_poly_link.link_id
399 pdbx_reference_entity_poly_link.atom_id_1
400 pdbx_reference_entity_poly_link.comp_id_1
401 pdbx_reference_entity_poly_link.entity_seq_num_1
402 pdbx_reference_entity_poly_link.atom_id_2
403 pdbx_reference_entity_poly_link.comp_id_2
404 pdbx_reference_entity_poly_link.entity_seq_num_2
405 pdbx_reference_entity_poly_link.value_order
406 pdbx_reference_entity_poly_link.component_id
407 pdbx_reference_entity_poly_link.value_order pdbx_struct_assembly.id
408 pdbx_reference_entity_poly_link.component_id pdbx_struct_assembly.details
409 pdbx_struct_assembly.id pdbx_struct_assembly.method_details

View File

@@ -0,0 +1,118 @@
# Tunnel Visualization Extension
This documentation outlines the usage of the Mol* extension for visualizing tunnels in molecular structures. The extension integrates with Mol* to render 3D representations of tunnels using specified data sources and properties.
The extension is a key component in ChannelsDB (https://channelsdb2.biodata.ceitec.cz/), enabling users to visualize tunnels within molecules directly from the database. While it is used with ChannelsDB, users can also input their own data or connect to different databases, ensuring versatility across various research environments.
## Data Types
The primary data types involved in tunnel visualization are:
### Tunnel
A Tunnel object contains the actual tunnel data necessary for visualization. It consists of:
- `data`: An array of `Profile` objects that describe the tunnel at various points.
- `props`: Properties such as the tunnel's type, ID, and optional labels or descriptions.
### Profile
A `Profile` object in a `Tunnel` holds detailed geometric and physical properties of a tunnel at specific points along its length. These properties include:
- `Charge`: The electric charge at a specific point in the tunnel.
- `Radius`: The overall radius of the tunnel at this point.
- `FreeRadius`: The radius of the tunnel not obstructed by any molecular elements.
- `T`: Temperature factor or a similar property related to the point.
- `Distance`: Distance along the tunnel's path from the start.
- `X`, `Y`, `Z`: Coordinates of the point in 3D space.
These profiles are crucial for understanding the physical and chemical environment inside the tunnel, allowing for detailed analysis and visualization.
Example:
```json
"Profile": [
{
"Radius": 1.49,
"FreeRadius": 1.49,
"T": 0,
"Distance": 0,
"X": -19.152,
"Y": -22.654,
"Z": -13.034,
"Charge": 0
},
{
"Radius": 1.524,
"FreeRadius": 1.524,
"T": 0.00625,
"Distance": 0.087,
"X": -19.162,
"Y": -22.596,
"Z": -12.969,
"Charge": 0
},
{
"Radius": 1.56,
"FreeRadius": 1.56,
"T": 0.0125,
"Distance": 0.174,
"X": -19.171,
"Y": -22.539,
"Z": -12.905,
"Charge": 0
}
]
```
## Transformers Usage
The extension uses several transformations to process and visualize tunnel data:
### Tunnels Data Transformer
- `Purpose`: Converts a collection of Tunnel data into a state object.
- `Usage`:
```typescript
update.toRoot().apply(TunnelsFromRawData, { data: tunnels });
```
### Tunnel Data Provider
- `Purpose`: Converts single Tunnel data into a state object for individual processing.
- `Usage`:
```typescript
update.toRoot().apply(TunnelFromRawData, {
data: {
data: tunnel.Profile,
props: { id: tunnel.Id, type: tunnel.Type }
}
});
```
### Tunnel Shape Provider
- `Purpose`: Provides the shapes for rendering the tunnel based on WebGL context and shape parameters.
- `Usage`:
```typescript
update.apply(TunnelShapeProvider, {
webgl,
}).apply(StateTransforms.Representation.ShapeRepresentation3D);
```
## Visualization Examples
To help users understand how to use these transformations in practice, include detailed examples:
### Visualizing Multiple Tunnels
This example ([runVisualizeTunnels](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L19)) demonstrates how to visualize multiple tunnels from a fetched dataset.
```typescript
update.toRoot()
.apply(TunnelsFromRawData, { data: tunnels })
.apply(SelectTunnel)
.apply(TunnelShapeProvider, { webgl })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
```
### Visualizing a Single Tunnel
This example ([runVisualizeTunnel](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L46)) shows how to visualize a single tunnel.
```typescript
update.toRoot()
.apply(TunnelFromRawData, {
data: {
data: tunnel.Profile,
props: { id: tunnel.Id, type: tunnel.Type }
}
})
.apply(TunnelShapeProvider, { webgl })
.apply(StateTransforms.Representation.ShapeRepresentation3D);
```

View File

@@ -53,6 +53,7 @@ nav:
- Extensions:
- MolViewSpec: 'extensions/mvs/index.md'
- wwPDB StructConn: 'extensions/struct-conn.md'
- Tunnels: 'extensions/tunnels.md'
- Misc:
- Interesting PDB entries: misc/interesting-pdb-entries.md
repo_url: https://github.com/molstar/docs

773
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "4.2.0",
"version": "4.5.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -106,7 +106,11 @@
"Yakov Pechersky <ffxen158@gmail.com>",
"Christian Dominguez <christian.99dominguez@gmail.com>",
"Cai Huiyu <szmun.caihy@gmail.com>",
"Ryan DiRisio <rjdiris@gmail.com>"
"Ryan DiRisio <rjdiris@gmail.com>",
"Dušan Veľký <dvelky@mail.muni.cz>",
"Neli Fonseca <neli@ebi.ac.uk>",
"Paul Pillot <paul.pillot@tandemai.com>",
"Herman Bergwerf <post@hbergwerf.nl>"
],
"license": "MIT",
"devDependencies": {
@@ -114,15 +118,15 @@
"@types/gl": "^6.0.5",
"@types/pngjs": "^6.0.5",
"@types/jest": "^29.5.12",
"@types/react": "^18.3.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"benchmark": "^2.1.4",
"concurrently": "^8.2.2",
"cpx2": "^7.0.1",
"crypto-browserify": "^3.12.0",
"css-loader": "^7.1.1",
"css-loader": "^7.1.2",
"eslint": "^8.57.0",
"extra-watch-webpack-plugin": "^1.0.3",
"file-loader": "^6.2.0",
@@ -135,14 +139,14 @@
"raw-loader": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.76.0",
"sass": "^1.77.8",
"sass-loader": "^14.2.1",
"simple-git": "^3.24.0",
"simple-git": "^3.25.0",
"stream-browserify": "^3.0.0",
"style-loader": "^4.0.0",
"ts-jest": "^29.1.2",
"typescript": "^5.4.5",
"webpack": "^5.91.0",
"ts-jest": "^29.2.3",
"typescript": "^5.5.4",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
@@ -150,9 +154,9 @@
"@types/benchmark": "^2.1.5",
"@types/compression": "1.7.5",
"@types/express": "^4.17.21",
"@types/node": "^18.19.31",
"@types/node": "^18.19.42",
"@types/node-fetch": "^2.6.11",
"@types/swagger-ui-dist": "3.30.4",
"@types/swagger-ui-dist": "3.30.5",
"argparse": "^2.0.1",
"body-parser": "^1.20.2",
"compression": "^1.7.4",
@@ -160,13 +164,13 @@
"express": "^4.19.2",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.1.1",
"immutable": "^4.3.5",
"immutable": "^4.3.7",
"io-ts": "^2.2.21",
"node-fetch": "^2.7.0",
"react-markdown": "^9.0.1",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.17.2",
"tslib": "^2.6.2",
"swagger-ui-dist": "^5.17.14",
"tslib": "^2.6.3",
"util.promisify": "^1.1.2",
"xhr2": "^0.2.1"
},

View File

@@ -42,7 +42,7 @@ function copyViewer() {
function copyMe() {
console.log('\n###', 'copy me files');
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
const meDeployPath = path.resolve(localPath, 'me/');
const meDeployPath = path.resolve(localPath, 'me/viewer/');
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
addAnalytics(path.resolve(meDeployPath, 'index.html'));
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -46,6 +46,7 @@ function occlusionStyle(plugin: PluginContext) {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'on', params: {
blurKernelSize: 15,
blurDepthBias: 0.5,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,

View File

@@ -30,6 +30,7 @@ import { Asset } from '../../mol-util/assets';
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
import { AnimateStateSnapshots } from '../../mol-plugin-state/animation/built-in/state-snapshots';
import { MesoViewportSnapshotDescription } from './ui/entities';
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
@@ -47,6 +48,7 @@ export type MesoscaleExplorerState = {
examples?: ExampleEntry[],
graphicsMode: GraphicsMode,
stateRef?: string,
driver?: any,
stateCache: { [k: string]: any },
}
@@ -90,7 +92,8 @@ const DefaultMesoscaleExplorerOptions = {
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
graphicsMode: 'quality' as GraphicsMode
graphicsMode: 'quality' as GraphicsMode,
driver: undefined
};
type MesoscaleExplorerOptions = typeof DefaultMesoscaleExplorerOptions;
@@ -170,6 +173,9 @@ export class MesoscaleExplorer {
right: RightPanel,
},
remoteState: 'none',
viewport: {
snapshotDescription: MesoViewportSnapshotDescription,
}
},
config: [
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
@@ -207,7 +213,12 @@ export class MesoscaleExplorer {
onBeforeUIRender: async plugin => {
let examples: MesoscaleExplorerState['examples'] = undefined;
try {
examples = await plugin.fetch({ url: './examples/list.json', type: 'json' }).run();
examples = await plugin.fetch({ url: '../examples/list.json', type: 'json' }).run();
// extend the array with file tour.json if it exists
const tour = await plugin.fetch({ url: '../examples/tour.json', type: 'json' }).run();
if (tour) {
examples = examples?.concat(tour);
}
} catch (e) {
console.log(e);
}
@@ -215,6 +226,7 @@ export class MesoscaleExplorer {
(plugin.customState as MesoscaleExplorerState) = {
examples,
graphicsMode: o.graphicsMode,
driver: o.driver,
stateCache: {},
};

View File

@@ -11,10 +11,12 @@ import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observ
import { Binding } from '../../../mol-util/binding';
import { PluginCommands } from '../../../mol-plugin/commands';
import { Sphere3D } from '../../../mol-math/geometry';
import { StructureElement } from '../../../mol-model/structure';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const Key = Binding.TriggerKey;
const DefaultMesoFocusLociBindings = {
clickCenter: Binding([
@@ -23,13 +25,14 @@ const DefaultMesoFocusLociBindings = {
clickCenterFocus: Binding([
Trigger(B.Flag.Secondary, M.create()),
], 'Camera center and focus', 'Click element using ${triggers}'),
keyCenterOnly: Binding([Key('C')], 'Center Only Toggle', 'Press ${triggers}'),
};
const MesoFocusLociParams = {
export const MesoFocusLociParams = {
minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
extraRadius: PD.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci' }),
durationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration' }),
centerOnly: PD.Boolean(true, { description: 'Keep current camera distance' }),
bindings: PD.Value(DefaultMesoFocusLociBindings, { isHidden: true }),
};
type MesoFocusLociProps = PD.Values<typeof MesoFocusLociParams>
@@ -47,25 +50,45 @@ export const MesoFocusLoci = PluginBehavior.create<MesoFocusLociProps>({
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const { clickCenter, clickCenterFocus } = this.params.bindings;
const { durationMs, extraRadius, minRadius } = this.params;
const { durationMs, extraRadius, minRadius, centerOnly } = this.params;
const radius = Math.max(sphere.radius + extraRadius, minRadius);
if (Binding.match(clickCenter, button, modifiers)) {
// left mouse button
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
if (StructureElement.Loci.is(current.loci)) {
if (centerOnly) {
const snapshot = canvas3d.camera.getCenter(sphere.center);
canvas3d.requestCameraReset({ durationMs, snapshot });
} else {
this.ctx.managers.camera.focusSphere(sphere, this.params);
}
}
} else if (Binding.match(clickCenterFocus, button, modifiers)) {
// right mouse button
if (Loci.isEmpty(current.loci)) {
PluginCommands.Camera.Reset(this.ctx, { });
return;
}
if (centerOnly) {
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
canvas3d.requestCameraReset({ durationMs, snapshot });
} else {
this.ctx.managers.camera.focusSphere(sphere, this.params);
}
}
});
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
canvas3d.requestCameraReset({ durationMs, snapshot });
this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ code, key, modifiers }) => {
if (!this.ctx.canvas3d) return;
const b = { ...DefaultMesoFocusLociBindings, ...this.params.bindings };
const { centerOnly } = this.params;
if (Binding.matchKey(b.keyCenterOnly, code, modifiers, key)) {
this.params.centerOnly = !centerOnly;
}
});
}

View File

@@ -16,12 +16,16 @@ import { StateTreeSpine } from '../../../mol-state/tree/spine';
import { Representation } from '../../../mol-repr/representation';
import { MarkerAction } from '../../../mol-util/marker-action';
import { PluginContext } from '../../../mol-plugin/context';
import { MesoscaleState, expandAllGroups, getCellDescription, getEveryEntity } from '../data/state';
const B = ButtonsType;
const M = ModifiersKeys;
const Trigger = Binding.Trigger;
const DefaultMesoSelectLociBindings = {
click: Binding([
Trigger(B.Flag.Primary, M.create())
], 'Click', 'Click element using ${triggers}'),
clickToggleSelect: Binding([
Trigger(B.Flag.Primary, M.create({ shift: true })),
Trigger(B.Flag.Primary, M.create({ control: true })),
@@ -63,15 +67,36 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
const { clickToggleSelect } = this.params.bindings;
const { click, clickToggleSelect } = this.params.bindings;
if (Binding.match(clickToggleSelect, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
this.ctx.managers.interactivity.lociSelects.deselectAll();
return;
}
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
const d = getCellDescription(cell!);
MesoscaleState.set(this.ctx, { focusInfo: `${d}` });
}
}
if (Binding.match(click, button, modifiers)) {
if (Loci.isEmpty(current.loci)) {
MesoscaleState.set(this.ctx, { focusInfo: '', filter: '' });
return;
}
const snapshotKey = current.repr?.props?.snapshotKey?.trim() ?? '';
if (snapshotKey) {
this.ctx.managers.snapshot.applyKey(snapshotKey);
} else {
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
const d = getCellDescription(cell!);
MesoscaleState.set(this.ctx, { focusInfo: `${d}`, filter: `${cell?.obj?.label}` });
expandAllGroups(this.ctx);
}
}
}
});
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
@@ -87,22 +112,41 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
this.ctx.managers.interactivity.lociHighlights.clearHighlights();
return;
}
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
if (StructureElement.Loci.is(current.loci)) {
if (modifiers.control) {
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
} else {
const loci = Loci.normalize(current.loci, 'chain');
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
}
}
}
if (Loci.isEmpty(current.loci)) {
this.ctx.behaviors.labels.highlight.next({ labels: [] });
this.ctx.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
} else {
const labels: string[] = [];
if (StructureElement.Loci.is(current.loci)) {
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
labels.push(cell?.obj?.label || 'Unknown');
const d = getCellDescription(cell!);
labels.push(d);
} else {
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
if (loci.kind === 'group-loci') {
if ('shape' in current.loci && current.loci.shape.geometry.kind === 'text') {
const qname = current.repr?.props.customText;
// highlight protein with same name
const entities = getEveryEntity(this.ctx, qname);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
this.ctx.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
labels.push(loci.shape.getLabel(0, 0));
}
}
this.ctx.behaviors.labels.highlight.next({ labels });
}

View File

@@ -161,15 +161,12 @@ const CellpackStructure = PluginStateTransform.BuiltIn({
const unitsByEntity = getUnitsByEntity(parent);
const units = unitsByEntity.get(idx) || [];
// if (!unitsByEntity.get(idx)) {
// console.log(entities.data.pdbx_description.value(idx));
// }
const structure = Structure.create(units);
const description = entities.data.pdbx_description.value(idx)[0] || 'model';
const label = description.split('.').at(-1) || a.label;
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
const description_label = entities.data.pdbx_description.value(idx)[0] || 'model';
const label = description_label.split('.').at(-1) || a.label;
const description = entities.data.pdbx_parent_entity_id.value(idx) || label;
return new PSO.Molecule.Structure(structure, { label, description: description }); // `${a.description}`
});
},
dispose({ b }) {

View File

@@ -12,7 +12,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../mol-state';
import { Color } from '../../../../mol-util/color';
import { ColorNames } from '../../../../mol-util/color/names';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
import { CellpackAssembly, CellpackStructure } from './model';
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, merge?: boolean) {
@@ -96,12 +96,12 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
const compRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const funcRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (entities._rowCount > 1) {
@@ -159,7 +159,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
parent.cell!.state.isCollapsed = false;
const group = await state.build()
.to(parent)
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
compGroups.set(n, group);
}
@@ -171,7 +171,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
const color = colorIdx !== undefined ? baseFuncColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(funcRoot)
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
funcGroups.set(f, group);
}
@@ -201,6 +201,9 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode), { tags: [`comp:${n}`, `func:${f}`] });
}
await build.commit();
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
await updateColors(plugin, values, options);
} catch (e) {
console.error(e);
plugin.log.error(e);

View File

@@ -76,6 +76,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
params: {
instances: PD.Value<GenericInstances<Asset>>(EmptyInstances),
label: PD.Optional(PD.Text('')),
description: PD.Optional(PD.Text('')),
cellSize: PD.Numeric(500, { min: 0, max: 10000, step: 100 }),
}
})({
@@ -111,7 +112,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
structure = assembler.getStructure();
}
const props = { label, description: Structure.elementDescription(structure) };
const props = { label, description: params.description || Structure.elementDescription(structure) };
return new SO.Molecule.Structure(structure, props);
});
},

View File

@@ -124,7 +124,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
async function addGroup(g: GenericGroup, cell: StateObjectSelector, parent: string) {
const group = await state.build()
.to(cell)
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, description: g.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
if (g.children) {
@@ -137,7 +137,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
for (const r of manifest.roots) {
const root = await state.build()
.toRoot()
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, description: r.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
if (r.children) {
@@ -193,11 +193,12 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
const file = Asset.File(new File([t], ent.file));
const color = ColorNames.skyblue;
const label = ent.label || ent.file.split('.')[0];
const sizeFactor = ent.sizeFactor || 1;
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
const instances = ent.instances && getAssetInstances(ent.instances);
const description = ent.description;
const label = ent.label || ent.file.split('.')[0];
build = build
.toRoot()
.apply(ReadFile, { file, label, isBinary });
@@ -233,7 +234,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
build = build
.apply(ModelFromTrajectory, { modelIndex: 0 })
.apply(StructureFromGeneric, { instances, label })
.apply(StructureFromGeneric, { instances, label, description })
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode, clipVariant), { tags });
} else if (['ply'].includes(info.ext)) {
if (['ply'].includes(info.ext)) {
@@ -250,9 +251,9 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
}
await build.commit();
const rootId = `${manifest.roots[0].id}:`;
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
await updateColors(plugin, values, rootId, '');
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
await updateColors(plugin, values, options);
} catch (e) {
console.error(e);
plugin.log.error(e);

View File

@@ -186,9 +186,10 @@ const MmcifStructure = PluginStateTransform.BuiltIn({
} else {
structure = Structure.create(units);
}
// could also use _struct_ref.pdbx_db_accession to point to uniprot with _struct_ref.db_name == UNP
const label = entities.data.pdbx_description.value(idx).join(', ') || 'model';
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
const description = `*Entity id* ${entities.data.id.value(idx)} *src_method* ${entities.data.src_method.value(idx)} *type* ${entities.data.type.value(idx)}`;
return new PSO.Molecule.Structure(structure, { label, description: description });
});
},
update({ newParams, oldParams }) {

View File

@@ -13,7 +13,7 @@ import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../m
import { Clip } from '../../../../mol-util/clip';
import { Color } from '../../../../mol-util/color';
import { ColorNames } from '../../../../mol-util/color/names';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
import { MmcifAssembly, MmcifStructure } from './model';
function getSpacefillParams(color: Color, scaleFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
@@ -114,7 +114,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
const entRoot = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit();
const getEntityType = (i: number) => {
@@ -148,7 +148,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
const color = colorIdx !== undefined ? baseEntColors[colorIdx] : ColorNames.white;
const group = await state.build()
.to(entRoot)
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
entGroups.set(t, group);
}
@@ -172,6 +172,9 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
.apply(StructureRepresentation3D, getSpacefillParams(color, scaleFactor, graphicsMode, clipVariant), { tags: [`ent:${t}`] });
}
await build.commit();
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
await updateColors(plugin, values, options);
} catch (e) {
console.error(e);
plugin.log.error(e);

View File

@@ -39,8 +39,10 @@ const StructureFromPetworld = PluginStateTransform.BuiltIn({
const { frame } = s.model.sourceData.data;
const pdbx_model = frame.categories.pdbx_model.getField('name')!;
const pdbx_description = frame.categories.pdbx_model.getField('description')!;
const description = pdbx_description ? pdbx_description.str(params.modelIndex) : Structure.elementDescription(s);
const label = pdbx_model.str(params.modelIndex);
const props = { label, description: Structure.elementDescription(s) };
const props = { label, description: description };
return new SO.Molecule.Structure(s, props);
});
},

View File

@@ -11,7 +11,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../../../mol-plugin/context';
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
import { ColorNames } from '../../../../mol-util/color/names';
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
import { Task } from '../../../../mol-task';
@@ -97,12 +97,12 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
const group = await state.build()
.toRoot()
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
.commit({ revertOnError: true });
await state.build()
.to(group)
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', illustrative: false, value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
.commit();
const colors = getDistinctBaseColors(other.length, 0);
@@ -124,6 +124,9 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: [`ent:`] });
}
await build.commit();
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
await updateColors(plugin, values, options);
} catch (e) {
console.error(e);
plugin.log.error(e);

View File

@@ -24,6 +24,7 @@ import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/rep
import { assertUnreachable } from '../../../mol-util/type-helpers';
import { MesoscaleExplorerState } from '../app';
import { saturate } from '../../../mol-math/interpolate';
import { Material } from '../../../mol-util/material';
function getHueRange(hue: number, variability: number) {
let min = hue - variability;
@@ -106,6 +107,7 @@ export function getDistinctBaseColors(count: number, shift: number, props?: Part
export const ColorParams = {
type: PD.Select('generate', PD.arrayToOptions(['generate', 'uniform', 'custom'])),
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type === 'custom' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
@@ -119,6 +121,7 @@ export const ColorValueParam = PD.Color(Color(0xFFFFFF));
export const RootParams = {
type: PD.Select('custom', PD.arrayToOptions(['group-generate', 'group-uniform', 'generate', 'uniform', 'custom'])),
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type !== 'uniform' }),
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'group-generate' }),
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
@@ -132,6 +135,10 @@ export const LightnessParams = {
};
export const DimLightness = 6;
export const IllustrativeParams = {
illustrative: PD.Boolean(false, { description: 'Illustrative style' }),
};
export const OpacityParams = {
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
@@ -140,11 +147,24 @@ export const EmissiveParams = {
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
};
export const celShaded = {
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' })
};
export type celShadedProps = PD.Values<typeof celShaded>;
export const PatternParams = {
frequency: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
amplitude: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
};
export const StyleParams = {
ignoreLight: PD.Boolean(false, { description: 'Ignore light for stylized rendering of representations' }),
materialStyle: Material.getParam(),
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' }),
};
export const LodParams = {
lodLevels: Spheres.Params.lodLevels,
cellSize: Spheres.Params.cellSize,
@@ -250,6 +270,7 @@ export const MesoscaleGroupParams = {
index: PD.Value<number>(-1, { isHidden: true }),
tag: PD.Value<string>('', { isHidden: true }),
label: PD.Value<string>('', { isHidden: true }),
description: PD.Value<string>('', { isHidden: true }),
hidden: PD.Boolean(false),
color: PD.Group(RootParams),
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
@@ -271,7 +292,7 @@ export const MesoscaleGroup = PluginStateTransform.BuiltIn({
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Apply Mesoscale Group', async () => {
return new MesoscaleGroupObject({}, { label: params.label });
return new MesoscaleGroupObject({}, { label: params.label, description: params.description });
});
},
});
@@ -360,7 +381,10 @@ export const MesoscaleStateParams = {
filter: PD.Value<string>('', { isHidden: true }),
graphics: PD.Select('quality', PD.arrayToOptions(['ultra', 'quality', 'balanced', 'performance', 'custom'] as GraphicsMode[])),
description: PD.Value<string>('', { isHidden: true }),
focusInfo: PD.Value<string>('', { isHidden: true }),
link: PD.Value<string>('', { isHidden: true }),
textSizeDescription: PD.Numeric(14, { min: 1, max: 100, step: 1 }, { isHidden: true }),
index: PD.Value<number>(-1, { isHidden: true })
};
export class MesoscaleStateObject extends PSO.Create<MesoscaleState>({ name: 'Mesoscale State', typeClass: 'Object' }) { }
@@ -447,7 +471,7 @@ export function getAllGroups(plugin: PluginContext, tag?: string) {
return _getAllGroups(plugin, tag, []);
}
export function getAllLeafGroups(plugin: PluginContext, tag: string) {
export function getAllLeafGroups(plugin: PluginContext, tag: string | undefined) {
const allGroups = getAllGroups(plugin, tag);
allGroups.sort((a, b) => a.params?.values.index - b.params?.values.index);
return allGroups.filter(g => {
@@ -481,7 +505,8 @@ function getFilterMatcher(filter: string) {
: new RegExp(escapeRegExp(filter), 'gi');
}
export function getFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
export function getFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
if (!filter) return getEntities(plugin, tag);
const matcher = getFilterMatcher(filter);
return getEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
@@ -498,21 +523,42 @@ export function getAllEntities(plugin: PluginContext, tag?: string) {
return _getAllEntities(plugin, tag, []);
}
export function getAllFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
export function getAllFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
if (!filter) return getAllEntities(plugin, tag);
const matcher = getFilterMatcher(filter);
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
}
export function getEveryEntity(plugin: PluginContext, filter?: string, tag?: string) {
if (filter) {
const matcher = getFilterMatcher(filter);
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
} else {
return getAllEntities(plugin, tag);
}
}
export function getEntityLabel(plugin: PluginContext, cell: StateObjectCell) {
return StateObjectRef.resolve(plugin.state.data, cell.transform.parent)?.obj?.label || 'Entity';
}
//
export function getCellDescription(cell: StateObjectCell) {
// markdown style for description
return '**' + cell?.obj?.label + '**\n\n' + cell?.obj?.description;
}
export async function updateColors(plugin: PluginContext, values: PD.Values, tag: string, filter: string) {
export function getEntityDescription(plugin: PluginContext, cell: StateObjectCell) {
const s = StateObjectRef.resolve(plugin.state.data, cell.transform.parent);
const d = getCellDescription(s!);
return d;
}
export async function updateColors(plugin: PluginContext, values: PD.Values, options?: PD.Values, tag?: string, filter?: string) {
const update = plugin.state.data.build();
const { type, value, shift, lightness, alpha, emissive } = values;
const { type, illustrative, value, shift, lightness, alpha, emissive } = values;
const doLighting = (options !== undefined);
const { ignoreLight, materialStyle: material, celShaded } = options ? options : { ignoreLight: true, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 }, celShaded: false };
if (type === 'group-generate' || type === 'group-uniform') {
const groups = getAllLeafGroups(plugin, tag);
const baseColors = getDistinctBaseColors(groups.length, shift);
@@ -531,11 +577,19 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
const c = type === 'group-generate' ? groupColors[j] : baseColors[i];
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
if (doLighting) {
old.type.params.ignoreLight = ignoreLight;
old.type.params.material = material;
old.type.params.celShaded = celShaded;
}
} else if (old.coloring) {
old.coloring.params.color = c;
old.coloring.params.lightness = lightness;
@@ -548,6 +602,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
update.to(g.transform.ref).update(old => {
old.color.type = type === 'group-generate' ? 'generate' : 'uniform';
old.color.illustrative = illustrative;
old.color.value = baseColors[i];
old.color.lightness = lightness;
old.color.alpha = alpha;
@@ -566,11 +621,19 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
const c = type === 'generate' ? groupColors[j] : value;
update.to(entities[j]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
if (doLighting) {
old.type.params.ignoreLight = ignoreLight;
old.type.params.material = material;
old.type.params.celShaded = celShaded;
}
} else if (old.coloring) {
old.coloring.params.color = c;
old.coloring.params.lightness = lightness;
@@ -585,6 +648,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
for (const o of others) {
update.to(o).update(old => {
old.color.type = type === 'generate' ? 'custom' : 'uniform';
old.color.illustrative = illustrative;
old.color.value = value;
old.color.lightness = lightness;
old.color.alpha = alpha;
@@ -603,3 +667,17 @@ export function expandAllGroups(plugin: PluginContext) {
}
}
};
export async function updateReprParams(plugin: PluginContext, options: PD.Values) {
const update = plugin.state.data.build();
const { ignoreLight, materialStyle: material, celShaded } = options;
const entities = getAllEntities(plugin);
for (let j = 0; j < entities.length; ++j) {
update.to(entities[j]).update(old => {
old.type.params.ignoreLight = ignoreLight;
old.type.params.material = material;
old.type.params.celShaded = celShaded;
});
}
await update.commit();
}

View File

@@ -4,6 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<link rel="icon" href="./favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="../extras/driver.css"/>
<title>Mol* Mesoscale Explorer</title>
<style>
* {
@@ -38,8 +39,11 @@
</head>
<body>
<div id="app"></div>
<script src="../extras/driver.js.iife.js"></script>
<script type="text/javascript" src="./molstar.js"></script>
<script type="text/javascript">
const driver = window.driver ? window.driver.js.driver() : undefined;
function getParam(name, regex) {
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
@@ -64,6 +68,7 @@
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
powerPreference: powerPreference || 'high-performance',
graphicsMode: graphicsMode || 'quality',
driver: driver
}).then(me => {
var example = getParam('example', '[^&]+').trim();
if (example) {
@@ -98,4 +103,4 @@
</script>
<!-- __MOLSTAR_ANALYTICS__ -->
</body>
</html>
</html>

View File

@@ -24,6 +24,7 @@ $logo-background: rgba(0,0,0,0.75);
}
@import 'mol-plugin-ui/skin/base/base';
@import 'mol-plugin-ui/skin/base/variables';
a {
color: $font-color;
@@ -31,3 +32,35 @@ a {
color: $hover-font-color;
}
}
.msp-snapshot-description-me {
background: rgba(red($default-background), green($default-background), blue($default-background), 0.5);
position: absolute;
height: 50vh; // 50% of the viewport height
left: 0;
top: $control-spacing + $row-height;
padding: (0.66 * $control-spacing) $control-spacing;
resize: both; /* Allows resizing in both directions */
overflow: auto; /* Adjust as needed */
a {
text-decoration: underline;
cursor: pointer;
color: $font-color;
}
ul, ol {
padding-left: $control-spacing + 4px;
}
&.hidden {
display: none;
}
&.shown {
display: block; // or 'flex', 'grid', etc. depending on your layout
}
}

View File

@@ -4,9 +4,9 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { PluginReactContext, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ControlGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg } from '../../../mol-plugin-ui/controls/icons';
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg, TooltipTextSvg, TooltipTextOutlineSvg, PlusBoxSvg, MinusBoxSvg } from '../../../mol-plugin-ui/controls/icons';
import { PluginCommands } from '../../../mol-plugin/commands';
import { State, StateObjectCell, StateSelection, StateTransformer } from '../../../mol-state';
import { ParameterControls, ParameterMappingControl, ParamOnChange, SelectControl } from '../../../mol-plugin-ui/controls/parameters';
@@ -18,14 +18,17 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
import { MarkerAction } from '../../../mol-util/marker-action';
import { EveryLoci, Loci } from '../../../mol-model/loci';
import { deepEqual } from '../../../mol-util';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams } from '../data/state';
import React from 'react';
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
import React, { useState } from 'react';
import { MesoscaleExplorerState } from '../app';
import { StructureElement } from '../../../mol-model/structure/structure/element';
import { PluginStateObject as PSO } from '../../../mol-plugin-state/objects';
import { Structure } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { Sphere3D } from '../../../mol-math/geometry';
import { MesoFocusLoci } from '../behavior/camera';
import Markdown from 'react-markdown';
import { combineLatest } from 'rxjs';
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
const { canvas3d } = plugin;
@@ -60,6 +63,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
if (!state.description && !state.link) return;
return {
selectionDescription: state.focusInfo,
description: state.description,
link: state.link,
};
@@ -68,7 +72,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
render() {
const info = this.info;
return info && <>
<div className='msp-help-text'>
<div id='modelinfo' className='msp-help-text'>
<div>{info.description}</div>
<div><a href={info.link} target='_blank'>Source</a></div>
</div>
@@ -101,17 +105,20 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
}
get info() {
const info: { label: string, key: string }[] = [];
const infos: { label: string, key: string, description?: string }[] = [];
this.plugin.managers.structure.selection.entries.forEach((e, k) => {
if (StructureElement.Loci.is(e.selection) && !StructureElement.Loci.isEmpty(e.selection)) {
const cell = this.plugin.helpers.substructureParent.get(e.selection.structure);
info.push({
const { entities } = e.selection.structure.model;
const description = entities.data.pdbx_description.value(0)[0] || 'model';
infos.push({
description: description,
label: cell?.obj?.label || 'Unknown',
key: k,
});
}
});
return info;
return infos;
}
find(label: string) {
@@ -133,31 +140,40 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
const loci = Structure.toStructureElementLoci(e.selection.structure);
centerLoci(this.plugin, loci);
const cell = this.plugin.helpers.substructureParent.get(loci.structure);
const d = getCellDescription(cell!); // '### ' + cell?.obj?.label + '\n\n' + cell?.obj?.description;
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
}
get selection() {
const info = this.info;
const help_selection = <><div>Use <i>ctrl+left</i> to select entities, either on the 3D canvas or in the tree below</div><div>Use <i>shift+left</i> to select individual chain on the 3D canvas</div></>;
if (!info.length) return <>
<div className='msp-help-text'>
<div>Use <i>ctrl+left click</i> to select entities, either on the 3D canvas or in the tree below</div>
<div id='seleinfo' className='msp-help-text'>
{help_selection}
</div>
</>;
return <>
{info.map((entry, index) => {
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
onClick={() => this.center(entry.key)}
>
<span title={entry.label}>{entry.label}</span>
</Button>;
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
return <div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
{label}
{find}
{remove}
</div>;
})}
<div id='seleinfo'>
{info.map((entry, index) => {
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
onClick={() => this.center(entry.key)}
>
<span title={entry.label}>
{entry.label}
</span>
</Button>;
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
return <>
<div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
{label}
{find}
{remove}
</div>
</>;
})};
</div>
</>;
}
@@ -216,7 +232,7 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
renderStyle() {
const style = this.style || '';
return <div style={{ margin: '5px', marginBottom: '10px' }}>
return <div id='selestyle' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Style'} param={SelectionStyleParam} value={style} onChange={(e) => { this.setStyle(e.value); }} />
</div>;
}
@@ -229,6 +245,192 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
}
}
export function MesoMarkdownAnchor({ href, children, element }: { href?: string, children?: any, element?: any }) {
const plugin = React.useContext(PluginReactContext);
if (!href) return element;
// Decode the href to handle encoded spaces and other characters
const decodedHref = href ? decodeURIComponent(href) : '';
const handleHover = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
if (decodedHref.startsWith('i')) {
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const query_names = decodedHref.substring(1).split(',');
for (const query_name of query_names) {
const entities = getEveryEntity(plugin, query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
} else if (decodedHref.startsWith('g')) {
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const qindex = decodedHref.indexOf('.');
const query = decodedHref.substring(1, qindex) + ':';
const query_names = decodedHref.substring(qindex + 1).split(',');
for (const query_name of query_names) {
const e = getAllEntities(plugin, query + query_name);
for (const r of e) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
}
}
}
};
const handleLeave = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
// Implement your hover off logic here
// Example: Perform an action if the href starts with 'h'
if (decodedHref.startsWith('i') || decodedHref.startsWith('g')) {
// Example hover off action
e.preventDefault();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
}
};
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
e.preventDefault();
if (href.startsWith('#')) {
plugin.managers.snapshot.applyKey(decodedHref.substring(1));
} else if (decodedHref.startsWith('i')) {
plugin.managers.interactivity.lociSelects.deselectAll();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const query_names = decodedHref.substring(1).split(',');
for (const query_name of query_names) {
const entities = getFilteredEntities(plugin, '', query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) {
return;
}
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
}
}
} else if (decodedHref.startsWith('g')) {
plugin.managers.interactivity.lociSelects.deselectAll();
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
const qindex = decodedHref.indexOf('.');
const query = decodedHref.substring(1, qindex) + ':';
const query_names = decodedHref.substring(qindex + 1).split(',');
for (const query_name of query_names) {
const entities = getAllEntities(plugin, query + query_name);
for (const r of entities) {
const repr = r.obj?.data.repr;
if (repr) {
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
}
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
if (!(cell?.obj?.data.sourceData instanceof Structure)) return;
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
}
}
} else {
// open the link in a new tab
window.open(decodedHref, '_blank');
}
};
if (decodedHref[0] === '#') {
return <a href={decodedHref[0]} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
}
if (decodedHref[0] === 'i' || decodedHref[0] === 'g') {
return <a href={decodedHref[0]} onMouseLeave={handleLeave} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
}
if (decodedHref[0] === 'h') {
return <a href={decodedHref[0]} onClick={handleClick} rel='noopener noreferrer'>{children}</a>;
}
return element;
}
export function MesoViewportSnapshotDescription() {
let tSize = 14;
const plugin = React.useContext(PluginReactContext);
if (MesoscaleState.has(plugin)) {
const state = MesoscaleState.get(plugin);
tSize = state.textSizeDescription;
}
const [_, setV] = React.useState(0);
const [isShown, setIsShown] = useState(true);
const [textSize, setTextSize] = useState(tSize);
const toggleVisibility = () => {
setIsShown(!isShown);
};
const increaseTextSize = () => {
setTextSize(prevSize => Math.min(prevSize + 2, 50)); // Increase the text size by 2px, but not above 50px
};
const decreaseTextSize = () => {
setTextSize(prevSize => Math.max(prevSize - 2, 2)); // Decrease the text size by 2px, but not below 2px
};
React.useEffect(() => {
const sub = plugin.managers.snapshot.events.changed.subscribe(() => setV(v => v + 1));
return () => sub.unsubscribe();
}, [plugin]);
const current = plugin.managers.snapshot.state.current;
if (!current) return null;
const e = plugin.managers.snapshot.getEntry(current)!;
if (!e?.description?.trim()) return null;
if (MesoscaleState.has(plugin)) {
MesoscaleState.set(plugin, { textSizeDescription: textSize });
}
const showInfo = <IconButton svg={isShown ? TooltipTextSvg : TooltipTextOutlineSvg} flex='20px' onClick={toggleVisibility} title={isShown ? 'Hide Description' : 'Show Description'}/>;
const increasePoliceSize = <IconButton svg={PlusBoxSvg} flex='20px' onClick={increaseTextSize} title='Bigger Text' />;
const decreasePoliceSize = <IconButton svg={MinusBoxSvg} flex='20px' onClick={decreaseTextSize} title='Smaller Text' />;
return (
<>
<div id='snapinfoctrl' className="msp-state-snapshot-viewport-controls" style={{ marginRight: '30px' }}>
{showInfo}{increasePoliceSize}{decreasePoliceSize}
</div>
<div id='snapinfo' className={`msp-snapshot-description-me ${isShown ? 'shown' : 'hidden'}`} style={{ fontSize: `${textSize}px` }}>
{<Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>}
</div>
</>
);
}
export class FocusInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
componentDidMount() {
this.subscribe(combineLatest([
this.plugin.state.data.behaviors.isUpdating,
this.plugin.managers.structure.selection.events.changed
]), ([isUpdating]) => {
if (!isUpdating) this.forceUpdate();
});
}
get info() {
let focusInfo = '';
if (MesoscaleState.has(this.plugin)) {
const state = MesoscaleState.get(this.plugin);
if (state.focusInfo) focusInfo = state.focusInfo;
}
return focusInfo;
}
render() {
const focusInfo = this.info;
const description = (focusInfo !== '') ? <Markdown skipHtml components={{ a: MesoMarkdownAnchor }}>{focusInfo}</Markdown> : '';
return <>
<div id='focusinfo' className='msp-help-text'>
{description}
</div>
</>;
}
}
export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean }> {
filterRef = React.createRef<HTMLInputElement>();
prevFilter = '';
@@ -336,7 +538,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
renderGraphics() {
const graphics = this.graphics;
return <div style={{ margin: '5px', marginBottom: '10px' }}>
return <div id='graphicsquality' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Graphics'} param={MesoscaleStateParams.graphics} value={`${graphics}`} onChange={(e) => { this.setGraphics(e.value); }} />
</div>;
}
@@ -363,7 +565,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
return <>
{this.renderGraphics()}
<div className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
<div id='searchtree' className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
<input type='text' ref={this.filterRef}
value={filter}
placeholder='Search'
@@ -373,10 +575,12 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
/>
<IconButton svg={CloseSvg} toggleState={false} disabled={disabled} onClick={() => this.setFilter('')} />
</div>
{options.length > 1 && <div style={{ margin: '5px', marginBottom: '10px' }}>
{options.length > 1 && <div id='grouptree' style={{ margin: '5px', marginBottom: '10px' }}>
<SelectControl name={'Group By'} param={groupParam} value={`${groupBy}`} onChange={(e) => { this.setGroupBy(parseInt(e.value)); }} />
</div>}
<GroupNode filter={filter} cell={root} depth={0} />
<div id='tree' style={{ position: 'relative', overflowY: 'auto', borderBottom: '1px solid #000', maxHeight: '600px' }}>
<GroupNode filter={filter} cell={root} depth={0} />
</div>
</>;
}
}
@@ -435,6 +639,12 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
this.setState({ action: this.state.action === 'root' ? undefined : 'root' });
};
showInfo = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
const d = getCellDescription(this.cell); // '### ' + this.cell?.obj?.label + '\n\n' + this.cell?.obj?.description;
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
};
highlight = (e: React.MouseEvent<HTMLElement>) => {
e.preventDefault();
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
@@ -493,7 +703,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
updateColor = (values: ColorProps) => {
const update = this.plugin.state.data.build();
const { value, type, lightness, alpha, emissive } = values;
const { value, illustrative, type, lightness, alpha, emissive } = values;
const entities = this.filteredEntities;
@@ -507,8 +717,11 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const c = type === 'generate' ? groupColors[i] : value;
update.to(entities[i]).update(old => {
if (old.type) {
old.colorTheme.params.value = c;
old.colorTheme.params.lightness = lightness;
if (illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness } } } };
} else {
old.colorTheme = { name: 'uniform', params: { value: c, lightness } };
}
old.type.params.alpha = alpha;
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
old.type.params.emissive = emissive;
@@ -536,7 +749,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
};
updateRoot = async (values: PD.Values) => {
await updateColors(this.plugin, values, this.cell.params?.values.tag, this.props.filter);
await updateColors(this.plugin, values, undefined, this.cell.params?.values.tag, this.props.filter);
const update = this.plugin.state.data.build();
@@ -655,6 +868,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={disabled}
onMouseEnter={this.highlight}
onMouseLeave={this.clearHighlight}
onClick={this.showInfo}
>
<span title={groupLabel}>{groupLabel}</span>
</Button>;
@@ -774,18 +988,63 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
if (e.ctrlKey) {
this.toggleSelect(e);
} else {
this.center(e);
const d = getEntityDescription(this.plugin, this.cell);
if (this.cell?.obj?.data.sourceData.state.models.length !== 0) {
const repr = this.cell?.obj?.data.repr;
if (repr) {
// for fiber need to think how to handle.
const aloci = repr.getAllLoci()[0];
const locis = Loci.normalize(aloci, 'chainInstances') as StructureElement.Loci;
const nChain = aloci.structure.state.unitSymmetryGroups.length;
let index = MesoscaleState.get(this.plugin).index + 1;
if (index * nChain >= locis.elements.length) index = 0;
const elems = locis.elements.slice(index * nChain, ((index + 1) * nChain)); // end index is not included
const loci = StructureElement.Loci(aloci.structure, elems);
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
const state = this.plugin.state.behaviors;
const selections = state.select(StateSelection.Generators.ofTransformer(MesoFocusLoci));
const params = selections.length === 1 ? selections[0].obj?.data.params : undefined;
if (!params.centerOnly) {
this.plugin.managers.camera.focusSphere(sphere, params);
} else {
const snapshot = this.plugin.canvas3d?.camera.getCenter(sphere.center);
this.plugin.canvas3d?.requestCameraReset({ durationMs: params.durationMs, snapshot });
}
MesoscaleState.set(this.plugin, { index: index, focusInfo: `${d}` });
}
} else {
this.center(e);
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
}
}
};
get colorValue(): Color | undefined {
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
if (this.cell.transform.params?.colorTheme?.params.value) {
return this.cell.transform.params?.colorTheme?.params.value;
} else if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
return this.cell.transform.params?.colorTheme?.params.style.params.value;
} else {
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
}
}
get illustrativeValue(): { illustrative: boolean } | undefined {
return {
illustrative: (this.cell.transform.params?.colorTheme?.name === 'illustrative')
};
}
get lightnessValue(): { lightness: number } | undefined {
return {
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
};
if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
return {
lightness: this.cell.transform.params?.colorTheme?.params.style.params.lightness ?? 0
};
} else {
return {
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
};
}
}
get opacityValue(): { alpha: number } | undefined {
@@ -838,7 +1097,11 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
}
update.to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.value = value;
if (old.colorTheme.name === 'illustrative') {
old.colorTheme.params.style.params.value = value;
} else {
old.colorTheme.params.value = value;
}
} else if (old.coloring) {
old.coloring.params.color = value;
}
@@ -846,10 +1109,26 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
update.commit();
};
updateIllustrative = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.colorTheme) {
if (old.colorTheme.name !== 'illustrative' && values.illustrative) {
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: old.colorTheme.params.value, lightness: old.colorTheme.params.lightness } } } };
} else if (old.colorTheme.name === 'illustrative' && !values.illustrative) {
old.colorTheme = { name: 'uniform', params: { value: old.colorTheme.params.style.params.value, lightness: old.colorTheme.params.style.params.lightness } };
}
}
}).commit();
};
updateLightness = (values: PD.Values) => {
return this.plugin.build().to(this.ref).update(old => {
if (old.colorTheme) {
old.colorTheme.params.lightness = values.lightness;
if (old.colorTheme.name === 'illustrative') {
old.colorTheme.params.style.params.lightness = values.lightness;
} else {
old.colorTheme.params.lightness = values.lightness;
}
} else if (old.coloring) {
old.coloring.params.lightness = values.lightness;
}
@@ -924,6 +1203,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
const depth = this.props.depth;
const colorValue = this.colorValue;
const lightnessValue = this.lightnessValue;
const illustrativeValue = this.illustrativeValue;
const opacityValue = this.opacityValue;
const emissiveValue = this.emissiveValue;
const lodValue = this.lodValue;
@@ -953,6 +1233,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
<CombinedColorControl param={ColorValueParam} value={colorValue ?? Color(0xFFFFFF)} onChange={this.updateColor} name='color' hideNameRow />
<ParameterControls params={IllustrativeParams} values={illustrativeValue} onChangeValues={this.updateIllustrative} />
<ParameterControls params={LightnessParams} values={lightnessValue} onChangeValues={this.updateLightness} />
<ParameterControls params={OpacityParams} values={opacityValue} onChangeValues={this.updateOpacity} />
<ParameterControls params={EmissiveParams} values={emissiveValue} onChangeValues={this.updateEmissive} />

View File

@@ -5,20 +5,63 @@
*/
import { Mp4EncoderUI } from '../../../extensions/mp4-export/ui';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { CollapsableControls, CollapsableState, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { SectionHeader } from '../../../mol-plugin-ui/controls/common';
import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
import { PluginCommands } from '../../../mol-plugin/commands';
import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/measurements';
import { MesoscaleExplorerState } from '../app';
import { MesoscaleState } from '../data/state';
import { EntityControls, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls } from './states';
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
import { RendererParams } from '../../../mol-gl/renderer';
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
const Spacer = () => <div style={{ height: '2em' }} />;
const ViewportParams = {
renderer: PD.Group(RendererParams),
trackball: PD.Group(TrackballControlsParams),
};
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
protected defaultState(): CollapsableState {
return {
header: 'Viewport Settings',
isCollapsed: true,
brand: { accent: 'cyan', svg: TuneSvg }
};
}
protected renderControls(): JSX.Element | null {
return <>
{this.plugin.canvas3d && this.plugin.canvas3dContext && <>
<ParameterControls params={ViewportParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
</>}
</>;
}
private setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
};
componentDidMount() {
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
}
}
export class LeftPanel extends PluginUIComponent {
render() {
const customState = this.plugin.customState as MesoscaleExplorerState;
return <div className='msp-scrollable-container'>
{customState.driver && <>
<ExplorerInfo />
<Spacer />
</>}
<SectionHeader title='Database' />
<DatabaseControls />
<Spacer />
@@ -42,6 +85,7 @@ export class LeftPanel extends PluginUIComponent {
<Spacer />
<Mp4EncoderUI />
<ViewportSettingsUI />
</div>;
}
}
@@ -59,6 +103,13 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
);
}
get hasFocusInfo() {
return (
MesoscaleState.has(this.plugin) &&
!!(MesoscaleState.get(this.plugin).focusInfo !== '')
);
}
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
@@ -89,10 +140,18 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
<SectionHeader title='Selection' />
<SelectionInfo />
<Spacer />
<StructureMeasurementsControls initiallyCollapsed={true}/>
</>
<MesoQuickStylesControls />
<Spacer />
<SectionHeader title='Entities' />
<EntityControls />
<Spacer />
{this.hasFocusInfo && <>
<SectionHeader title='Focus Info' />
<FocusInfo />
<Spacer />
</>}
</div>;
}
}
}

View File

@@ -1,15 +1,16 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
import { StructureComponentManager } from '../../../mol-plugin-state/manager/structure/component';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
import { Button, ExpandGroup } from '../../../mol-plugin-ui/controls/common';
import { GetAppSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
import { PluginCommands } from '../../../mol-plugin/commands';
@@ -24,7 +25,7 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
import { createGenericHierarchy } from '../data/generic/preset';
import { createMmcifHierarchy } from '../data/mmcif/preset';
import { createPetworldHierarchy } from '../data/petworld/preset';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps } from '../data/state';
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateColors } from '../data/state';
function adjustPluginProps(ctx: PluginContext) {
ctx.managers.interactivity.setProps({ granularity: 'chain' });
@@ -77,6 +78,7 @@ function adjustPluginProps(ctx: PluginContext) {
radius: 5,
bias: 1,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
@@ -163,7 +165,19 @@ export async function loadExampleEntry(ctx: PluginContext, entry: ExampleEntry)
export async function loadUrl(ctx: PluginContext, url: string, type: 'molx' | 'molj' | 'cif' | 'bcif') {
if (type === 'molx' || type === 'molj') {
const customState = ctx.customState as MesoscaleExplorerState;
delete customState.stateRef;
customState.stateCache = {};
ctx.managers.asset.clear();
await PluginCommands.State.Snapshots.Clear(ctx);
await PluginCommands.State.Snapshots.OpenUrl(ctx, { url, type });
const cell = ctx.state.data.selectQ(q => q.ofType(MesoscaleStateObject))[0];
if (!cell) throw new Error('Missing MesoscaleState');
customState.stateRef = cell.transform.ref;
customState.graphicsMode = cell.obj?.data.graphics || customState.graphicsMode;
} else {
await reset(ctx);
const isBinary = type === 'bcif';
@@ -269,7 +283,7 @@ export class DatabaseControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='database' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadDatabase} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -281,7 +295,7 @@ export class LoaderControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='loader' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadModel} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -293,7 +307,7 @@ export class ExampleControls extends PluginUIComponent {
}
render() {
return <div style={{ margin: '5px' }}>
return <div id='example' style={{ margin: '5px' }}>
<ApplyActionControl state={this.plugin.state.data} action={LoadExample} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
</div>;
}
@@ -330,7 +344,7 @@ export class SessionControls extends PluginUIComponent {
};
render() {
return <div style={{ margin: '5px' }}>
return <div id='session' style={{ margin: '5px' }}>
<div className='msp-flex-row'>
<Button icon={GetAppSvg} onClick={this.downloadToFileZip} title='Download the state.'>
Download
@@ -346,14 +360,14 @@ export class SessionControls extends PluginUIComponent {
export class SnapshotControls extends PluginUIComponent<{}> {
render() {
return <div style={{ margin: '5px' }}>
<div style={{ marginBottom: '10px' }}>
<div id='snaplist' style={{ marginBottom: '10px' }}>
<LocalStateSnapshotList />
</div>
<div style={{ marginBottom: '10px' }}>
<div id='snap' style={{ marginBottom: '10px' }}>
<LocalStateSnapshots />
</div>
<div style={{ marginBottom: '10px' }}>
<div id='snapoption' style={{ marginBottom: '10px' }}>
<ExpandGroup header='Snapshot Options' initiallyExpanded={false}>
<LocalStateSnapshotParams />
</ExpandGroup>
@@ -361,3 +375,496 @@ export class SnapshotControls extends PluginUIComponent<{}> {
</div>;
}
}
export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, showHelp: boolean }> {
state = {
isDisabled: false,
showHelp: false
};
componentDidMount() {
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
this.setState({ isDisabled: v });
});
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
if (!this.state.isDisabled && MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref) {
this.forceUpdate();
}
});
}
setupDriver = () => {
// setup the tour of the interface
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver) return;
driver.setSteps([
// Left panel
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
{ element: '#snaplist', popover: { title: 'Snapshot List', description: 'View and manage the list of snapshots. You can reorder them and edit their titles, keys, and descriptions. Snapshot states cannot be edited.', side: 'right', align: 'start' } },
{ element: '#snap', popover: { title: 'Add Snapshot', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
{ element: '#snapoption', popover: { title: 'Snapshot Options', description: 'These options are saved in the snapshot. Set them before adding a snapshot to see their effect during animation playback.', side: 'right', align: 'start' } },
{ element: '#exportanimation', popover: { title: 'Export Animation', description: 'Create movies or scenes with rocking, rotating, or snapshots animations.', side: 'right', align: 'start' } },
{ element: '#viewportsettings', popover: { title: 'Viewport Settings', description: 'Advanced settings for the renderer and trackball.', side: 'right', align: 'start' } },
// Viewport
{ element: '#snapinfo', popover: { title: 'Snapshot Description', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
{ element: '#snapinfoctrl', popover: { title: 'Snapshot Description Control', description: 'Control the visibility and text size of the snapshot description widget.', side: 'right', align: 'start' } },
// Right panel
{ element: '#modelinfo', popover: { title: 'Model Information', description: 'Summary information about the model, if available.', side: 'right', align: 'start' } },
{ element: '#selestyle', popover: { title: 'Selection Style', description: 'Choose the rendering style for entity selection accessed via Shift/Ctrl mouse. Options include: Color & Outline, Color, Outline.', side: 'right', align: 'start' } },
{ element: '#seleinfo', popover: { title: 'Selection List', description: 'View the current list of selected entities.', side: 'right', align: 'start' } },
{ element: '#measurements', popover: { title: 'Measurements', description: 'Use this widget to create labels, measure distances, angles, dihedral orientations, and planes for the selected entities.', side: 'right', align: 'start' } },
{ element: '#quickstyles', popover: { title: 'Quick Styles', description: 'Change between a selection of style presets.', side: 'right', align: 'start' } },
{ element: '#graphicsquality', popover: { title: 'Graphics Quality', description: 'Adjust the overall graphics quality. Lower quality improves performance. Options are: Ultra, Quality (Default), Balanced, Performance, Custom. Custom settings use the Culling & LOD values set in the Tree.', side: 'right', align: 'start' } },
{ element: '#searchtree', popover: { title: 'Search', description: 'Filter the entity tree based on your queries.', side: 'right', align: 'start' } },
{ element: '#grouptree', popover: { title: 'Group By', description: 'Change the grouping of the hierarchy tree, e.g., group by instance or by compartment.', side: 'right', align: 'start' } },
{ element: '#tree', popover: { title: 'Tree Hierarchy', description: 'View the hierarchical tree of entity types in the model.', side: 'right', align: 'start' } },
{ element: '#focusinfo', popover: { title: 'Selection Description', description: 'Detailed information about the current selection, if present in the loaded file.', side: 'right', align: 'start' } },
{ popover: { title: 'Happy Exploring!', description: 'Thats all! Go ahead and start exploring or creating mesoscale tours.' } }
]);
driver.refresh();
};
openHelp = () => {
// open a new page with the documentation
window.open('https://molstar.org/me-docs/', '_blank');
};
toggleHelp = () => {
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver || !driver.hasNextStep()) {
this.setupDriver();
}
this.setState({ showHelp: !this.state.showHelp }, () => {
if (this.state.showHelp && driver) {
driver.drive(); // start at 0
}
});
};
render() {
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
if (!driver) return;
const help = <IconButton svg={HelpOutlineSvg} toggleState={false} small onClick={this.openHelp} title='Open the Documentation' />;
const tour = <IconButton svg={TourSvg} toggleState={false} small onClick={this.toggleHelp} title='Start the interactive tour' />;
return <>
<div id='explorerinfo' style={{ display: 'flex', alignItems: 'center', padding: '4px 0 4px 8px' }} className='msp-help-text'>
<h2 style={{ flexGrow: 1 }}>Mol* Mesoscale Explorer</h2>
{tour}{help}
</div>
</>;
}
}
export class MesoQuickStylesControls extends CollapsableControls {
defaultState() {
return {
isCollapsed: true,
header: 'Quick Styles',
brand: { accent: 'gray' as const, svg: MagicWandSvg }
};
}
renderControls() {
return <>
<MesoQuickStyles />
</>;
}
}
export class MesoQuickStyles extends PluginUIComponent {
state = {
celShaded: false,
};
default_color_values = {
type: 'group-generate',
illustrative: false,
value: [1, 1, 1, 1],
variability: 20,
shift: 0,
lightness: 0,
alpha: 1,
emissive: 0
};
illustrative_color_values = {
type: 'group-generate',
illustrative: true,
value: [1, 1, 1, 1],
variability: 20,
shift: 0,
lightness: 0,
alpha: 1,
emissive: 0
};
async default() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.6,
maxDistance: 80,
steps: 3,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
const options = { ...loptions, celShaded: false, };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.default_color_values, options);
}
async celshading() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1.5,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.4,
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: { name: 'off', params: {} },
dof: { name: 'off', params: {} },
}
});
// ignore Light
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
const options = { ...loptions, celShaded: true, };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.default_color_values, options);
}
async stylizedDof() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1.3,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.4,
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: { name: 'off', params: {} },
dof: {
name: 'on',
params: {
blurSize: 9,
blurSpread: 1.0,
inFocus: 0.0,
PPM: 200.0,
center: 'camera-target',
mode: 'sphere',
}
}
}
});
// ignore Light
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
const options = { ...loptions, celShaded: false };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.default_color_values, options);
}
async illustrative() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1.5,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.4,
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
// ignore Light
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
const options = { ...loptions, celShaded: false, };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.illustrative_color_values, options);
}
async shiny() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.5,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1.3,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: { name: 'off', params: {} },
outline: { name: 'off', params: {} },
dof: { name: 'off', params: {} },
}
});
// ignore Light
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
const options = { ...loptions, celShaded: false };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.default_color_values, options);
}
async stylized() {
if (!this.plugin.canvas3d) return;
this.plugin.canvas3d.setProps({
renderer: {
exposure: 1.1,
},
postprocessing: {
occlusion: {
name: 'on',
params: {
samples: 32,
multiScale: {
name: 'on',
params: {
levels: [
{ radius: 2, bias: 1.0 },
{ radius: 5, bias: 1.0 },
{ radius: 8, bias: 1.0 },
{ radius: 11, bias: 1.0 },
],
nearThreshold: 10,
farThreshold: 1500,
}
},
radius: 5,
bias: 1.3,
blurKernelSize: 11,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}
},
shadow: {
name: 'on',
params: {
bias: 0.4,
maxDistance: 256,
steps: 64,
tolerance: 1.0,
}
},
outline: {
name: 'on',
params: {
scale: 1,
threshold: 0.15,
color: Color(0x000000),
includeTransparent: false,
}
},
dof: { name: 'off', params: {} },
}
});
// ignore Light
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
const options = { ...loptions, celShaded: false };
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
await updateColors(this.plugin, this.illustrative_color_values, options);
}
render() {
return <>
<div className='msp-flex-row'>
<Button noOverflow title='Applies default representation preset and sets outline and occlusion effects to default' onClick={() => this.default()} style={{ width: 'auto' }}>
Default
</Button>
<Button noOverflow title='Applies celShading' onClick={() => this.celshading()} style={{ width: 'auto' }}>
Cel-shaded
</Button>
<Button noOverflow title='Applies illustrative colors preset' onClick={() => this.illustrative()} style={{ width: 'auto' }}>
Illustrative
</Button>
</div>
<div className='msp-flex-row'>
<Button noOverflow title='Apply shiny material to default' onClick={() => this.shiny()} style={{ width: 'auto' }}>
Shiny
</Button>
<Button noOverflow title='Enable shiny material, outline, and illustrative colors' onClick={() => this.stylized()} style={{ width: 'auto' }}>
Shiny-Illustrative
</Button>
<Button noOverflow title='Enable DOF and shiny material' onClick={() => this.stylizedDof()} style={{ width: 'auto' }}>
Shiny-DOF
</Button>
</div>
</>;
}
}

View File

@@ -3,6 +3,7 @@
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Neli Fonseca <neli@ebi.ac.uk>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
@@ -21,7 +22,7 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
@@ -81,6 +82,7 @@ export const ExtensionMap = {
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
'mvs': PluginSpec.Behavior(MolViewSpec),
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
};
const DefaultViewerOptions = {
@@ -422,6 +424,34 @@ export class Viewer {
});
}
loadFullResolutionEMDBMap(emdbId: string, options: { isoValue: Volume.IsoValue, color?: Color }) {
const plugin = this.plugin;
const numericId = parseInt(emdbId.toUpperCase().replace('EMD-', ''));
const url = `https://ftp.ebi.ac.uk/pub/databases/emdb/structures/EMD-${numericId}/map/emd_${numericId}.map.gz`;
return plugin.dataTransaction(async () => {
const data = await plugin.build().toRoot()
.apply(StateTransforms.Data.Download, { url, isBinary: true, label: emdbId }, { state: { isGhost: true } })
.apply(StateTransforms.Data.DeflateData)
.commit();
const parsed = await plugin.dataFormats.get('ccp4')!.parse(plugin, data, { entryId: emdbId });
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
await plugin.build()
.to(volume)
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
type: 'isosurface',
typeParams: { alpha: 1, isoValue: options.isoValue },
color: 'uniform',
colorParams: { value: options.color ?? Color(0x33BB33) }
}))
.commit();
});
}
/**
* @example
* viewer.loadTrajectory({

View File

@@ -234,13 +234,19 @@ const FORCE_INT_FIELDS = [
'_atom_site.id',
'_atom_site.auth_seq_id',
'_atom_site_anisotrop.id',
'_atom_site_anisotrop.pdbx_auth_seq_id',
'_pdbx_struct_mod_residue.auth_seq_id',
'_pdbx_unobs_or_zero_occ_residues.auth_seq_id',
'_struct_conf.beg_auth_seq_id',
'_struct_conf.end_auth_seq_id',
'_struct_conn.ptnr1_auth_seq_id',
'_struct_conn.ptnr2_auth_seq_id',
'_struct_sheet_range.beg_auth_seq_id',
'_struct_sheet_range.end_auth_seq_id',
'_struct_site.pdbx_auth_seq_id',
'_struct_site_gen.auth_seq_id',
'_struct_mon_prot_cis.auth_seq_id',
'_struct_mon_prot_cis.pdbx_auth_seq_id_2',
];
/**

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -33,6 +33,7 @@ const Canvas3DPresets = {
radius: 5,
bias: 0.8,
blurKernelSize: 15,
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Mandar Deshpande <mandar@ebi.ac.uk>
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
@@ -41,8 +41,13 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
const getColor = (location: StructureElement.Location): Color => {
const { unit, element } = location;
if (!Unit.isAtomic(unit)) return DefaultColor;
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
const score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
let score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
if (typeof score !== 'number') {
score = unit.model.atomicConformation.B_iso_or_equiv.value(element);
}
if (score < 0) {
return DefaultColor;
} else if (score <= 50) {
@@ -74,7 +79,7 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
preferSmoothing: true,
color,
props,
description: 'Assigns residue colors according to the pLDDT Confidence score.',
description: 'Assigns residue colors according to the pLDDT Confidence score. If no Model Archive quality assessment score is available, the B-factor value is used instead.',
legend: ConfidenceColorLegend
};
}
@@ -86,7 +91,7 @@ export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfide
factory: PLDDTConfidenceColorTheme,
getParams: getPLDDTConfidenceColorThemeParams,
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')),
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT') || m.atomicConformation.B_iso_or_equiv.isDefined),
ensureCustomProperties: {
attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => {
if (data.structure) {

View File

@@ -72,10 +72,10 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
await params.pass.updateBackground();
await plugin.managers.animation.play(params.animation.definition, params.animation.params);
stoppedAnimation = false;
for (let i = 0; i <= N; i++) {
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true });
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true, updateControls: true });
const image = params.pass.getImageData(width, height, normalizedViewport);
encoder.addFrameRgba(image.data);

View File

@@ -1,3 +1,12 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dominik Tichý <tichydominik451@gmail.com>
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
export { SbNcbrPartialCharges } from './partial-charges/behavior';
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
export { SbNcbrTunnels } from './tunnels/behavior';
export { TunnelsFromRawData, SelectTunnel, TunnelFromRawData, TunnelShapeProvider } from './tunnels/representation';

View File

@@ -0,0 +1,99 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { PluginContext } from '../../../mol-plugin/context';
import { StateAction } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { Asset } from '../../../mol-util/assets';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { assertUnreachable } from '../../../mol-util/type-helpers';
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
import { TunnelsServerConfig } from './props';
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider } from './representation';
export const TunnelDownloadServer = {
'channelsdb': PD.EmptyGroup({ label: 'ChannelsDB' })
};
type DownloadTunnels = typeof DownloadTunnels
export const DownloadTunnels = StateAction.build({
display: { name: 'Download Tunnels', description: 'Load a tunnels from the provided source and create theirs representations' },
from: PluginStateObject.Root,
params: (_, plugin: PluginContext) => {
return {
source: PD.MappedStatic('pdb', {
'pdb': PD.Group({
provider: PD.Group({
id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma/space separated PDB ids.' }),
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
}, { pivot: 'id' }),
}, { isFlat: true, label: 'PDB' }),
'alphafolddb': PD.Group({
provider: PD.Group({
id: PD.Text('Q8W3K0', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
}, { pivot: 'id' })
}, { isFlat: true, label: 'AlphaFold DB', description: 'Loads the predicted model if available' }),
'url': PD.Group({
url: PD.Url(''),
}, { isFlat: true, label: 'URL' })
})
};
}
})(({ params, state }, plugin: PluginContext) => Task.create('Download Tunnels', async ctx => {
plugin.behaviors.layout.leftPanelTabName.next('data');
const src = params.source;
let downloadParams: PD.Normalize<{ url: string | Asset.Url }>[];
switch (src.name) {
case 'url':
downloadParams = [{ url: src.params.url }];
break;
case 'pdb':
downloadParams = src.params.provider.server.name === 'channelsdb'
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/pdb/${src.params.provider.id}` }]
: assertUnreachable(src as never);
break;
case 'alphafolddb':
downloadParams = src.params.provider.server.name === 'channelsdb'
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/alphafill/${src.params.provider.id.toLowerCase()}` }]
: assertUnreachable(src as never);
break;
default: assertUnreachable(src);
}
await state.transaction(async () => {
const update = plugin.build();
const webgl = plugin.canvas3dContext?.webgl;
for (const download of downloadParams) {
const response = await (await fetch(download.url.toString())).json();
const tunnels: Tunnel[] = [];
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
if (values.length > 0) {
values.forEach((item: TunnelDB) => {
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
});
}
});
update
.toRoot()
.apply(TunnelsFromRawData, { data: tunnels })
.apply(SelectTunnel)
.apply(TunnelShapeProvider, {
webgl,
})
.apply(StateTransforms.Representation.ShapeRepresentation3D);
await update.commit();
}
}).runInContext(ctx);
}));

View File

@@ -0,0 +1,607 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { OrderedSet } from '../../../mol-data/int';
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 { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/algorithm';
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { Texture } from '../../../mol-gl/webgl/texture';
import { PositionData, Sphere3D, Box3D, GridLookup3D, fillGridDim } from '../../../mol-math/geometry';
import { Boundary, getBoundary } from '../../../mol-math/geometry/boundary';
import { DefaultMolecularSurfaceCalculationProps, MolecularSurfaceCalculationProps } from '../../../mol-math/geometry/molecular-surface';
import { lerp, spline } from '../../../mol-math/interpolate';
import { Vec3, Tensor, Mat4 } from '../../../mol-math/linear-algebra';
import { Shape } from '../../../mol-model/shape';
import { ensureReasonableResolution } from '../../../mol-repr/structure/visual/util/common';
import { Task, RuntimeContext } from '../../../mol-task';
import { ValueCell } from '../../../mol-util';
import { Color } from '../../../mol-util/color';
import { Tunnel, Profile } from './data-model';
type MolecularSurfaceMeta = {
resolution?: number
colorTexture?: Texture
}
export async function createSpheresShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, showRadii: boolean, prev?: Shape<Mesh> }) {
const builder = MeshBuilder.createState(512, 512, options.prev?.geometry);
const tunnel = options.tunnel;
const processedData = interpolateTunnel(tunnel.data, options.sampleRate);
if (options.showRadii) {
for (let i = 0; i < processedData.length; i += 1) {
const p = processedData[i];
builder.currentGroup = i;
const center = [p.X, p.Y, p.Z];
addSphere(builder, center as Vec3, p.Radius, options.resolution);
}
} else {
for (let i = 0; i < processedData.length; i += 1) {
const p = processedData[i];
builder.currentGroup = 0;
const center = [p.X, p.Y, p.Z];
addSphere(builder, center as Vec3, p.Radius, options.resolution);
}
}
const mesh = MeshBuilder.getMesh(builder);
const name = tunnel.props.highlight_label ?
tunnel.props.highlight_label :
tunnel.props.type && tunnel.props.id ?
`${tunnel.props.type} ${tunnel.props.id}` :
'Tunnel';
if (options.showRadii)
return Shape.create(
name,
tunnel.props,
mesh,
() => Color(options.color),
() => 1,
(i) => `[${processedData[i].X.toFixed(3)}, ${processedData[i].Y.toFixed(3)}, ${processedData[i].Z.toFixed(3)}] - radius: ${processedData[i].Radius.toFixed(3)}`,
);
return Shape.create(
name,
tunnel.props,
mesh,
() => Color(options.color),
() => 1,
() => name,
);
}
export async function createTunnelShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, webgl: WebGLContext | undefined, prev?: Shape<Mesh> }) {
const tunnel = options.tunnel;
const mesh = await createTunnelMesh(tunnel.data, {
detail: options.resolution,
sampleRate: options.sampleRate,
webgl: options.webgl,
prev: options.prev?.geometry
});
const name = tunnel.props.highlight_label ?
tunnel.props.highlight_label :
tunnel.props.type && tunnel.props.id ?
`${tunnel.props.type} ${tunnel.props.id}` :
'Tunnel';
return Shape.create(
name,
tunnel.props,
mesh,
() => Color(options.color),
() => 1,
() => name,
);
}
function profileToVec3(profile: Profile): Vec3 {
return Vec3.create(profile.X, profile.Y, profile.Z);
}
// Centripetal CatmullRom spline interpolation
function interpolateTunnel(profile: Profile[], sampleRate: number) {
const interpolatedProfiles: Profile[] = [];
if (profile.length < 4) return profile; // Ensuring there are enough points to interpolate
interpolatedProfiles.push(profile[0]);
let lastPoint = profileToVec3(profile[0]);
let currentDistance = 0;
const pointInterval = 1 / sampleRate;
for (let i = 1; i < profile.length - 2; i++) {
const P0 = profile[i - 1];
const P1 = profile[i];
const P2 = profile[i + 1];
const P3 = profile[i + 2];
for (let t = 0; t <= 1; t += 0.05) {
const interpolatedX = spline(P0.X, P1.X, P2.X, P3.X, t, 0.5);
const interpolatedY = spline(P0.Y, P1.Y, P2.Y, P3.Y, t, 0.5);
const interpolatedZ = spline(P0.Z, P1.Z, P2.Z, P3.Z, t, 0.5);
const interpolatedPoint = Vec3.create(interpolatedX, interpolatedY, interpolatedZ);
const distanceToAdd = Vec3.distance(lastPoint, interpolatedPoint);
currentDistance += distanceToAdd;
if (currentDistance >= pointInterval) {
interpolatedProfiles.push({
X: interpolatedX,
Y: interpolatedY,
Z: interpolatedZ,
Radius: spline(P0.Radius, P1.Radius, P2.Radius, P3.Radius, t, 0.5),
Charge: lerp(P1.Charge, P2.Charge, t),
FreeRadius: spline(P0.FreeRadius, P1.FreeRadius, P2.FreeRadius, P3.FreeRadius, t, 0.5),
T: lerp(P1.T, P2.T, t),
Distance: lerp(P1.Distance, P2.Distance, t)
});
lastPoint = interpolatedPoint;
currentDistance -= pointInterval;
}
}
}
// Ensuring the last profile point is included
interpolatedProfiles.push(profile[profile.length - 1]);
return interpolatedProfiles;
}
function convertToPositionData(profile: Profile[], probeRadius: number): Required<PositionData> {
let position = {} as PositionData;
const x: number[] = [];
const y: number[] = [];
const z: number[] = [];
const indices: Array<number> = [];
const radius: number[] = [];
let maxRadius: number = Number.MIN_SAFE_INTEGER;
let sphereCounter = 0;
for (const sphere of profile) {
x.push(sphere.X);
y.push(sphere.Y);
z.push(sphere.Z);
indices.push(sphereCounter);
radius.push(sphere.Radius + probeRadius);
if (sphere.Radius > maxRadius) maxRadius = sphere.Radius;
sphereCounter++;
}
position = { x, y, z, indices: OrderedSet.ofSortedArray(indices), radius, id: indices };
return position as Required<PositionData>;
}
async function createTunnelMesh(
data: Profile[],
options: {
detail: number,
sampleRate: number,
webgl?: WebGLContext,
prev?: Mesh
}
) {
const props = {
...DefaultMolecularSurfaceCalculationProps,
};
const preprocessedData = interpolateTunnel(data, options.sampleRate);
const positions = convertToPositionData(preprocessedData, props.probeRadius);
const bounds: Boundary = getBoundary(positions);
let maxR = 0;
for (let i = 0; i < positions.radius.length; ++i) {
const r = positions.radius[i];
if (maxR < r) maxR = r;
}
const p = ensureReasonableResolution(bounds.box, props);
const { field, transform, /* resolution,*/ maxRadius, /* idField */ } = await computeTunnelSurface(
{
positions,
boundary: bounds,
maxRadius: maxR,
box: bounds.box,
props: p
}
).run();
const params = {
isoLevel: p.probeRadius,
scalarField: field,
};
const surface = await computeMarchingCubesMesh(params, options.prev).run();
const iterations = Math.ceil(2 / 1);
Mesh.smoothEdges(surface, { iterations, maxNewEdgeLength: Math.sqrt(2) });
Mesh.transform(surface, transform);
if (options.webgl && !options.webgl.isWebGL2) {
Mesh.uniformTriangleGroup(surface);
ValueCell.updateIfChanged(surface.varyingGroup, false);
} else {
ValueCell.updateIfChanged(surface.varyingGroup, true);
}
const sphere = Sphere3D.expand(Sphere3D(), bounds.sphere, maxRadius);
surface.setBoundingSphere(sphere);
(surface.meta as MolecularSurfaceMeta).resolution = options.detail;
return surface;
}
function normalToLine(out: Vec3, p: Vec3) {
out[0] = out[1] = out[2] = 1.0;
if (p[0] !== 0) {
out[0] = (p[1] + p[2]) / -p[0];
} else if (p[1] !== 0) {
out[1] = (p[0] + p[2]) / -p[1];
} else if (p[2] !== 0) {
out[2] = (p[0] + p[1]) / -p[2];
}
return out;
}
function computeTunnelSurface(
surfaceData: {
positions: Required<PositionData>,
boundary: Boundary,
maxRadius: number,
box: Box3D | null,
props: MolecularSurfaceCalculationProps
}
) {
return Task.create('Tunnel Surface', async (ctx) => {
return await calcTunnelSurface(ctx, surfaceData);
});
}
type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
function getAngleTables(probePositions: number): AnglesTables {
let theta = 0.0;
const step = 2 * Math.PI / probePositions;
const cosTable = new Float32Array(probePositions);
const sinTable = new Float32Array(probePositions);
for (let i = 0; i < probePositions; i++) {
cosTable[i] = Math.cos(theta);
sinTable[i] = Math.sin(theta);
theta += step;
}
return { cosTable, sinTable };
}
// From '../../../\mol-math\geometry\molecular-surface.ts'
async function calcTunnelSurface(ctx: RuntimeContext,
surfaceData: {
positions: Required<PositionData>,
boundary: Boundary,
maxRadius: number,
box: Box3D | null,
props: MolecularSurfaceCalculationProps
}) {
// Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
// Other parts based heavily on NGL (Alexander Rose) EDT Surface class
let lastClip = -1;
/**
* Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
* Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
*
* Cache the last clipped atom (as very often the same one in subsequent calls)
*
* `a` and `b` must be resolved indices
*/
function obscured(x: number, y: number, z: number, a: number, b: number) {
if (lastClip !== -1) {
const ai = lastClip;
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
return ai;
} else {
lastClip = -1;
}
}
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
const ai = OrderedSet.getAt(indices, neighbours.indices[j]);
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
lastClip = ai;
return ai;
}
}
return -1;
}
/**
* `ai` must be a resolved index
*/
function singleAtomObscures(ai: number, x: number, y: number, z: number) {
const r = radius[ai];
const dx = px[ai] - x;
const dy = py[ai] - y;
const dz = pz[ai] - z;
const dSq = dx * dx + dy * dy + dz * dz;
return dSq < (r * r);
}
/**
* For each atom:
* Iterate over a subsection of the grid, for each point:
* If current value < 0.0, unvisited, set positive
*
* In any case: Project this point onto surface of the atomic sphere
* If this projected point is not obscured by any other atom
* Calculate delta distance and set grid value to minimum of
* itself and delta
*/
function projectPointsRange(begI: number, endI: number) {
for (let i = begI; i < endI; ++i) {
const j = OrderedSet.getAt(indices, i);
const vx = px[j], vy = py[j], vz = pz[j];
const rad = radius[j];
const rSq = rad * rad;
lookup3d.find(vx, vy, vz, rad);
// Number of grid points, round this up...
const ng = Math.ceil(rad * scaleFactor);
// Center of the atom, mapped to grid points (take floor)
const iax = Math.floor(scaleFactor * (vx - minX));
const iay = Math.floor(scaleFactor * (vy - minY));
const iaz = Math.floor(scaleFactor * (vz - minZ));
// Extents of grid to consider for this atom
const begX = Math.max(0, iax - ng);
const begY = Math.max(0, iay - ng);
const begZ = Math.max(0, iaz - ng);
// Add two to these points:
// - iax are floor'd values so this ensures coverage
// - these are loop limits (exclusive)
const endX = Math.min(dimX, iax + ng + 2);
const endY = Math.min(dimY, iay + ng + 2);
const endZ = Math.min(dimZ, iaz + ng + 2);
for (let xi = begX; xi < endX; ++xi) {
const dx = gridx[xi] - vx;
const xIdx = xi * iuv;
for (let yi = begY; yi < endY; ++yi) {
const dy = gridy[yi] - vy;
const dxySq = dx * dx + dy * dy;
const xyIdx = yi * iu + xIdx;
for (let zi = begZ; zi < endZ; ++zi) {
const dz = gridz[zi] - vz;
const dSq = dxySq + dz * dz;
if (dSq < rSq) {
const idx = zi + xyIdx;
// if unvisited, make positive
if (data[idx] < 0.0) data[idx] *= -1;
// Project on to the surface of the sphere
// sp is the projected point ( dx, dy, dz ) * ( ra / d )
const d = Math.sqrt(dSq);
const ap = rad / d;
const spx = dx * ap + vx;
const spy = dy * ap + vy;
const spz = dz * ap + vz;
if (obscured(spx, spy, spz, j, -1) === -1) {
const dd = rad - d;
if (dd < data[idx]) {
data[idx] = dd;
idData[idx] = id[i];
}
}
}
}
}
}
}
}
async function projectPoints() {
for (let i = 0; i < n; i += updateChunk) {
projectPointsRange(i, Math.min(i + updateChunk, n));
if (ctx.shouldUpdate) {
await ctx.update({ message: 'projecting points', current: i, max: n });
}
}
}
// Vectors for Torus Projection
const atob = Vec3();
const mid = Vec3();
const n1 = Vec3();
const n2 = Vec3();
/**
* `a` and `b` must be resolved indices
*/
function projectTorus(a: number, b: number) {
const rA = radius[a];
const rB = radius[b];
const dx = atob[0] = px[b] - px[a];
const dy = atob[1] = py[b] - py[a];
const dz = atob[2] = pz[b] - pz[a];
const dSq = dx * dx + dy * dy + dz * dz;
// This check now redundant as already done in AVHash.withinRadii
// if (dSq > ((rA + rB) * (rA + rB))) { return }
const d = Math.sqrt(dSq);
// Find angle between a->b vector and the circle
// of their intersection by cosine rule
const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d);
// distance along a->b at intersection
const dmp = rA * cosA;
Vec3.normalize(atob, atob);
// Create normal to line
normalToLine(n1, atob);
Vec3.normalize(n1, n1);
// Cross together for second normal vector
Vec3.cross(n2, atob, n1);
Vec3.normalize(n2, n2);
// r is radius of circle of intersection
const rInt = Math.sqrt(rA * rA - dmp * dmp);
Vec3.scale(n1, n1, rInt);
Vec3.scale(n2, n2, rInt);
Vec3.scale(atob, atob, dmp);
mid[0] = atob[0] + px[a];
mid[1] = atob[1] + py[a];
mid[2] = atob[2] + pz[a];
lastClip = -1;
for (let i = 0; i < probePositions; ++i) {
const cost = cosTable[i];
const sint = sinTable[i];
const px = mid[0] + cost * n1[0] + sint * n2[0];
const py = mid[1] + cost * n1[1] + sint * n2[1];
const pz = mid[2] + cost * n1[2] + sint * n2[2];
if (obscured(px, py, pz, a, b) === -1) {
const iax = Math.floor(scaleFactor * (px - minX));
const iay = Math.floor(scaleFactor * (py - minY));
const iaz = Math.floor(scaleFactor * (pz - minZ));
const begX = Math.max(0, iax - ngTorus);
const begY = Math.max(0, iay - ngTorus);
const begZ = Math.max(0, iaz - ngTorus);
const endX = Math.min(dimX, iax + ngTorus + 2);
const endY = Math.min(dimY, iay + ngTorus + 2);
const endZ = Math.min(dimZ, iaz + ngTorus + 2);
for (let xi = begX; xi < endX; ++xi) {
const dx = px - gridx[xi];
const xIdx = xi * iuv;
for (let yi = begY; yi < endY; ++yi) {
const dy = py - gridy[yi];
const dxySq = dx * dx + dy * dy;
const xyIdx = yi * iu + xIdx;
for (let zi = begZ; zi < endZ; ++zi) {
const dz = pz - gridz[zi];
const dSq = dxySq + dz * dz;
const idx = zi + xyIdx;
const current = data[idx];
if (current > 0.0 && dSq < (current * current)) {
data[idx] = Math.sqrt(dSq);
// Is this grid point closer to a or b?
// Take dot product of atob and gridpoint->p (dx, dy, dz)
const dp = dx * atob[0] + dy * atob[1] + dz * atob[2];
idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)];
}
}
}
}
}
}
}
function projectToriiRange(begI: number, endI: number) {
for (let i = begI; i < endI; ++i) {
const k = OrderedSet.getAt(indices, i);
lookup3d.find(px[k], py[k], pz[k], radius[k]);
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
const l = OrderedSet.getAt(indices, neighbours.indices[j]);
if (k < l) projectTorus(k, l);
}
}
}
async function projectTorii() {
for (let i = 0; i < n; i += updateChunk) {
projectToriiRange(i, Math.min(i + updateChunk, n));
if (ctx.shouldUpdate) {
await ctx.update({ message: 'projecting torii', current: i, max: n });
}
}
}
// console.time('MolecularSurface')
// console.time('MolecularSurface createState')
const { resolution, probeRadius, probePositions } = surfaceData.props;
const scaleFactor = 1 / resolution;
const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
const cellSize = Vec3.create(surfaceData.maxRadius, surfaceData.maxRadius, surfaceData.maxRadius);
Vec3.scale(cellSize, cellSize, 2);
const lookup3d = GridLookup3D(surfaceData.positions, surfaceData.boundary, cellSize);
const neighbours = lookup3d.result;
if (surfaceData.box === null) surfaceData.box = lookup3d.boundary.box;
const { indices, x: px, y: py, z: pz, id, radius } = surfaceData.positions;
const n = OrderedSet.size(indices);
const pad = surfaceData.maxRadius + resolution;
const expandedBox = Box3D.expand(Box3D(), surfaceData.box, Vec3.create(pad, pad, pad));
const [minX, minY, minZ] = expandedBox.min;
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
const dim = Box3D.size(Vec3(), scaledBox);
Vec3.ceil(dim, dim);
const [dimX, dimY, dimZ] = dim;
const iu = dimZ, iv = dimY, iuv = iu * iv;
const { cosTable, sinTable } = getAngleTables(probePositions);
const space = Tensor.Space(dim, [0, 1, 2], Float32Array);
const data = space.create();
const idData = space.create();
data.fill(-1001.0);
idData.fill(-1);
const gridx = fillGridDim(dimX, minX, resolution);
const gridy = fillGridDim(dimY, minY, resolution);
const gridz = fillGridDim(dimZ, minZ, resolution);
const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(surfaceData.maxRadius, 3), 3) * scaleFactor)));
// console.timeEnd('MolecularSurface createState')
// console.time('MolecularSurface projectPoints')
await projectPoints();
// console.timeEnd('MolecularSurface projectPoints')
// console.time('MolecularSurface projectTorii')
await projectTorii();
// console.timeEnd('MolecularSurface projectTorii')
// console.timeEnd('MolecularSurface')
const field = Tensor.create(space, data);
const idField = Tensor.create(space, idData);
const transform = Mat4.identity();
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
Mat4.setTranslation(transform, expandedBox.min);
// console.log({ field, idField, transform, updateChunk })
return { field, idField, transform, resolution, maxRadius: surfaceData.maxRadius };
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { PluginBehavior } from '../../../mol-plugin/behavior';
import { DownloadTunnels } from './actions';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { Model, Structure } from '../../../mol-model/structure';
import { PluginContext } from '../../../mol-plugin/context';
import { StateObjectRef } from '../../../mol-state';
import { getTunnelsConfig, TunnelsDataParams } from './props';
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { Tunnel, ChannelsDBdata, TunnelDB } from './data-model';
import { TunnelShapeProvider, TunnelFromRawData } from './representation';
import { ColorGenerator } from '../../meshes/mesh-utils';
export const SbNcbrTunnels = PluginBehavior.create<{ autoAttach: boolean }>({
name: 'sb-ncbr-tunnels',
category: 'misc',
display: {
name: 'SB NCBR Tunnels',
},
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
register(): void {
this.ctx.state.data.actions.add(DownloadTunnels);
this.ctx.builders.structure.representation.registerPreset(TunnelsPreset);
}
unregister() {
this.ctx.state.data.actions.remove(DownloadTunnels);
this.ctx.builders.structure.representation.unregisterPreset(TunnelsPreset);
}
},
params: () => ({
autoAttach: PD.Boolean(true),
})
});
export function isApplicable(structure?: Structure): boolean {
return (
!!structure && structure.models.length === 1 &&
Model.hasPdbId(structure.models[0])
);
}
export const TunnelsPreset = StructureRepresentationPresetProvider({
id: 'sb-ncbr-preset-structure-tunnels',
display: {
name: 'Tunnels', group: 'Annotation',
description: 'Shows Tunnels from ChannelsDB contained in the structure.'
},
isApplicable(a) {
return isApplicable(a.data);
},
params: (a, plugin) => {
return {
...StructureRepresentationPresetProvider.CommonParams,
...getConfiguredDefaultParams(plugin)
};
},
async apply(ref, params, plugin) {
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
const structure = structureCell?.obj?.data;
if (!structureCell || !structure) return {};
const update = plugin.build();
const webgl = plugin.canvas3dContext?.webgl;
const response = await (await fetch(`${params.serverUrl}/channels/${params.serverType}/${structure.model.entryId.toLowerCase()}`)).json();
const tunnels: Tunnel[] = [];
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
if (values.length > 0) {
values.forEach((item: TunnelDB) => {
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
});
}
});
await tunnels.forEach(async (tunnel) => {
await update
.toRoot()
.apply(TunnelFromRawData, { data: tunnel })
.apply(TunnelShapeProvider, {
webgl,
colorTheme: ColorGenerator.next().value,
})
.apply(StateTransforms.Representation.ShapeRepresentation3D);
await update.commit();
});
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
return { components: preset.components, representations: { ...preset.representations, } };
}
});
function getConfiguredDefaultParams(plugin: PluginContext) {
const config = getTunnelsConfig(plugin);
const params = PD.clone(TunnelsDataParams);
PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
return params;
}

View File

@@ -0,0 +1,119 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { WebGLContext } from '../../../mol-gl/webgl/context';
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { Color } from '../../../mol-util/color';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export interface Profile {
Charge: number,
Radius: number,
FreeRadius: number,
T: number,
Distance: number,
X: number,
Y: number,
Z: number
}
export interface Layerweightedproperties {
Hydrophobicity: number,
Hydropathy: number,
Polarity: number,
Mutability: number
}
export interface LayerGeometry {
MinRadius: number,
MinFreeRadius: number,
StartDistance: number,
EndDistance: number,
LocalMinimum: boolean,
Bottleneck: boolean,
bottleneck: boolean
}
export interface Properties {
Charge: number,
NumPositives: number,
NumNegatives: number,
Hydrophobicity: number,
Hydropathy: number,
Polarity: number,
Mutability: number
}
export interface LayersInfo {
LayerGeometry: LayerGeometry,
Residues: string[],
FlowIndices: string[],
Properties: Properties
}
export interface Layers {
ResidueFlow: string[],
HetResidues: any[],
LayerWeightedProperties: Layerweightedproperties
LayersInfo: LayersInfo[]
}
export interface TunnelDB {
Type: string,
Id: string,
Cavity: string,
Auto: boolean,
Properties: Properties,
Profile: Profile[],
Layers: Layers
};
export interface ChannelsDBdata {
'CSATunnels_MOLE': TunnelDB[],
'CSATunnels_Caver': TunnelDB[],
'ReviewedChannels_MOLE': TunnelDB[],
'ReviewedChannels_Caver': TunnelDB[],
'CofactorTunnels_MOLE': TunnelDB[],
'CofactorTunnels_Caver': TunnelDB[],
'TransmembranePores_MOLE': TunnelDB[],
'TransmembranePores_Caver': TunnelDB[],
'ProcognateTunnels_MOLE': TunnelDB[],
'ProcognateTunnels_Caver': TunnelDB[],
'AlphaFillTunnels_MOLE': TunnelDB[],
'AlphaFillTunnels_Caver': TunnelDB[]
}
export interface ChannelsCache {
Channels: ChannelsDBdata
}
export interface Tunnel {
data: Profile[],
props: {
highlight_label?: string,
type?: string,
id?: string,
label?: string,
description?: string
}
}
export const TunnelShapeParams = {
webgl: PD.Value<WebGLContext | null>(null),
colorTheme: PD.Color(Color(0xff0000)),
visual: PD.MappedStatic(
'mesh',
{
mesh: PD.Group({ resolution: PD.Numeric(2) }),
spheres: PD.Group({ resolution: PD.Numeric(2) })
}
),
samplingRate: PD.Numeric(1, { min: 0.05, max: 1, step: 0.05 }),
showRadii: PD.Boolean(false),
};
export class TunnelStateObject extends PluginStateObject.Create<{ tunnel: Tunnel }>({ name: 'Tunnel Entry', typeClass: 'Data' }) { }
export class TunnelsStateObject extends PluginStateObject.Create<{ tunnels: Tunnel[] }>({ name: 'Tunnels', typeClass: 'Data' }) { }

View File

@@ -0,0 +1,63 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { PluginContext } from '../../../mol-plugin/context';
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider, TunnelFromRawData } from './representation';
export const DB_URL = 'https://channelsdb2.biodata.ceitec.cz/api/channels/';
export const SUB_DB = 'pdb';
export const CHANNEL = '1ymg';
export const URL = `${DB_URL}${SUB_DB}/${CHANNEL}`;
export async function runVisualizeTunnels(plugin: PluginContext, url: string = URL) {
const update = plugin.build();
const webgl = plugin.canvas3dContext?.webgl;
const response = await (await fetch(url)).json();
const tunnels: Tunnel[] = [];
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
if (values.length > 0) {
values.forEach((item: TunnelDB) => {
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
});
}
});
update
.toRoot()
.apply(TunnelsFromRawData, { data: tunnels })
.apply(SelectTunnel)
.apply(TunnelShapeProvider, {
webgl,
})
.apply(StateTransforms.Representation.ShapeRepresentation3D);
await update.commit();
}
export async function runVisualizeTunnel(plugin: PluginContext) {
const update = plugin.build();
const webgl = plugin.canvas3dContext?.webgl;
const response = await (await fetch(URL)).json();
const tunnel = response.Channels.TransmembranePores_MOLE[0];
update
.toRoot()
.apply(TunnelFromRawData, { data: { data: tunnel.Profile, props: { id: tunnel.Id, type: tunnel.Type } } })
.apply(TunnelShapeProvider, {
webgl,
})
.apply(StateTransforms.Representation.ShapeRepresentation3D);
await update.commit();
}

View File

@@ -0,0 +1,27 @@
import { PluginConfigItem } from '../../../mol-plugin/config';
import { PluginContext } from '../../../mol-plugin/context';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export namespace TunnelsData {
export const DefaultServerUrl = 'https://channelsdb2.biodata.ceitec.cz/api';
export const DefaultServerType = 'pdb';
}
export const TunnelsDataParams = {
serverType: PD.Select('pdb', [['pdb', 'pdb']] as const),
serverUrl: PD.Text(TunnelsData.DefaultServerUrl)
};
export type TunnelsDataParams = typeof TunnelsDataParams
export const TunnelsServerConfig = {
DefaultServerUrl: new PluginConfigItem('channelsdb-server', 'https://channelsdb2.biodata.ceitec.cz/api'),
DefaultServerType: new PluginConfigItem<'pdb'>('serverType', 'pdb')
};
export function getTunnelsConfig(plugin: PluginContext): { [key in keyof typeof TunnelsServerConfig]: NonNullable<typeof TunnelsServerConfig[key]['defaultValue']> } {
return {
DefaultServerUrl: plugin.config.get(TunnelsServerConfig.DefaultServerUrl) ?? TunnelsServerConfig.DefaultServerUrl.defaultValue ?? TunnelsData.DefaultServerUrl,
DefaultServerType: plugin.config.get(TunnelsServerConfig.DefaultServerType) ?? TunnelsServerConfig.DefaultServerType.defaultValue ?? TunnelsData.DefaultServerType,
};
}

View File

@@ -0,0 +1,102 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Dušan Veľký <dvelky@mail.muni.cz>
*/
import { PluginStateObject } from '../../../mol-plugin-state/objects';
import { StateTransformer } from '../../../mol-state';
import { TunnelStateObject, Tunnel, TunnelShapeParams, TunnelsStateObject } from './data-model';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { Task } from '../../../mol-task';
import { createTunnelShape, createSpheresShape } from './algorithm';
const Transform = StateTransformer.builderFactory('sb-ncbr-tunnels');
export const TunnelsFromRawData = Transform({
name: 'sb-ncbr-tunnels-from-data',
display: { name: 'Tunnels' },
from: PluginStateObject.Root,
to: TunnelsStateObject,
params: {
data: PD.Value<Tunnel[]>([]),
},
})({
apply({ params }) {
return new TunnelsStateObject({ tunnels: params.data });
},
});
export const SelectTunnel = Transform({
name: 'sb-ncbr-tunnel-from-tunnels',
display: { name: 'Tunnel Selection' },
from: TunnelsStateObject,
to: TunnelStateObject,
params: a => {
return {
index: PD.Numeric(0, { min: 0, max: a!.data.tunnels.length - 1, step: 1 })
};
}
})({
apply({ a, params }) {
return new TunnelStateObject({ tunnel: a.data.tunnels[params.index] });
}
});
export const TunnelFromRawData = Transform({
name: 'sb-ncbr-tunnel-from-data',
display: { name: 'Tunnel Entry' },
from: PluginStateObject.Root,
to: TunnelStateObject,
params: {
data: PD.Value<Tunnel>(undefined as any, { isHidden: true })
},
})({
apply({ params }) {
return new TunnelStateObject({ tunnel: params.data });
},
});
export const TunnelShapeProvider = Transform({
name: 'sb-ncbr-tunnel-shape-provider',
display: { name: 'Tunnel' },
from: TunnelStateObject,
to: PluginStateObject.Shape.Provider,
params: a => { return TunnelShapeParams; },
})({
apply({ a, params }) {
return Task.create('Tunnel Shape Representation', async ctx => {
return new PluginStateObject.Shape.Provider({
label: 'Surface',
data: { params, data: a.data },
params: Mesh.Params,
geometryUtils: Mesh.Utils,
getShape: (_, data, __, mesh) => {
if (data.params.visual.name === 'mesh' && !data.params.showRadii) {
return createTunnelShape({
tunnel: data.data.tunnel,
color: data.params.colorTheme,
resolution: data.params.visual.params.resolution,
sampleRate: data.params.samplingRate,
webgl: data.params.webgl, prev: mesh
});
}
return createSpheresShape({
tunnel: data.data.tunnel,
color: data.params.colorTheme,
resolution: data.params.visual.params.resolution,
sampleRate: data.params.samplingRate,
showRadii: data.params.showRadii, prev: mesh
});
}
}, {
label: a.data.tunnel.props.label ?? 'Tunnel',
description: a.data.tunnel.props.description
?? (a.data.tunnel.props.type && a.data.tunnel.props.id)
? `${a.data.tunnel.props.type} ${a.data.tunnel.props.id}`
: '',
});
});
},
});

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 Mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
@@ -87,22 +87,48 @@ class CameraTransitionManager {
namespace CameraTransitionManager {
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot) => void
const _rot = Quat.identity();
const _rotUp = Quat.identity();
const _rotDist = Quat.identity();
const _sourcePosition = Vec3();
const _targetPosition = Vec3();
export function defaultTransition(out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot): void {
Camera.copySnapshot(out, target);
// Rotate up
Quat.slerp(_rot, Quat.Identity, Quat.rotationTo(_rot, source.up, target.up), t);
Vec3.transformQuat(out.up, source.up, _rot);
Quat.slerp(_rotUp, Quat.Identity, Quat.rotationTo(_rotUp, source.up, target.up), t);
Vec3.transformQuat(out.up, source.up, _rotUp);
// Lerp target, position & radius
Vec3.lerp(out.target, source.target, target.target, t);
Vec3.lerp(out.position, source.position, target.position, t);
// Interpolate distance
const distSource = Vec3.distance(source.target, source.position);
const distTarget = Vec3.distance(target.target, target.position);
const dist = lerp(distSource, distTarget, t);
// Rotate between source and targer direction
Vec3.sub(_sourcePosition, source.position, source.target);
Vec3.normalize(_sourcePosition, _sourcePosition);
Vec3.sub(_targetPosition, target.position, target.target);
Vec3.normalize(_targetPosition, _targetPosition);
Quat.rotationTo(_rotDist, _sourcePosition, _targetPosition);
Quat.slerp(_rotDist, Quat.Identity, _rotDist, t);
Vec3.transformQuat(_sourcePosition, _sourcePosition, _rotDist);
Vec3.scale(_sourcePosition, _sourcePosition, dist);
Vec3.add(out.position, out.target, _sourcePosition);
// Interpolate radius
out.radius = lerp(source.radius, target.radius, t);
// TODO take change of `clipFar` into account
out.radiusMax = lerp(source.radiusMax, target.radiusMax, t);
// Lerp fov & fog
// Interpolate fov & fog
out.fov = lerp(source.fov, target.fov, t);
out.fog = lerp(source.fog, target.fog, t);
}

View File

@@ -278,7 +278,7 @@ interface Canvas3D {
* Function for external "animation" control
* Calls commit.
*/
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }): void
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
clear(): void
syncVisibility(): void
@@ -527,9 +527,15 @@ namespace Canvas3D {
let animationFrameHandle = 0;
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }) {
currentTime = t;
commit(options?.isSynchronous);
// update the controler before the camera transition
if (options?.updateControls) {
controls.update(currentTime);
}
camera.transition.tick(currentTime);
hiZ.tick();

View File

@@ -3,6 +3,7 @@
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Herman Bergwerf <post@hbergwerf.nl>
*
* This code has been modified from https://github.com/mrdoob/three.js/,
* copyright (c) 2010-2018 three.js authors. MIT License
@@ -10,7 +11,7 @@
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
import { Viewport } from '../camera/util';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, 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';
@@ -67,7 +68,7 @@ export const TrackballControlsParams = {
animate: PD.MappedStatic('off', {
off: PD.EmptyGroup(),
spin: PD.Group({
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }, { description: 'Rotation speed in radians per second' }),
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
rock: PD.Group({
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
@@ -134,7 +135,6 @@ namespace TrackballControls {
const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
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);
@@ -614,18 +614,27 @@ namespace TrackballControls {
}
}
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
_isInteracting = true;
function onPinch({ isStart, startX, startY, centerPageX, centerPageY, fractionDelta, buttons, modifiers }: PinchInput) {
if (outsideViewport(startX, startY)) return;
const pan = Binding.match(b.dragPan, buttons, modifiers);
const zoom = Binding.match(b.scrollZoom, buttons, modifiers);
_isInteracting = pan || zoom;
if (pan) {
getMouseOnScreen(centerPageX, centerPageY);
if (isStart) {
Vec2.copy(_panStart, mouseOnScreenVec2);
Vec2.copy(_panEnd, _panStart);
} else {
Vec2.copy(_panEnd, mouseOnScreenVec2);
}
}
if (zoom) {
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
}
}
function onGesture({ deltaScale }: GestureInput) {
_isInteracting = true;
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
}
function onMove({ movementX, movementY }: MoveInput) {
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
@@ -804,7 +813,6 @@ namespace TrackballControls {
dragSub.unsubscribe();
wheelSub.unsubscribe();
pinchSub.unsubscribe();
gestureSub.unsubscribe();
interactionEndSub.unsubscribe();
keyDownSub.unsubscribe();
keyUpSub.unsubscribe();
@@ -817,8 +825,8 @@ namespace TrackballControls {
function spin(deltaT: number) {
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
const frameSpeed = p.animate.params.speed / 1000;
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
const radPerMs = p.animate.params.speed / 1000;
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
}

View File

@@ -0,0 +1,212 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Ludovic Autin <autin@scripps.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
import { TextureSpec, UniformSpec, DefineSpec, Values } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { Texture, createNullTexture } from '../../mol-gl/webgl/texture';
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
import { ValueCell } from '../../mol-util';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { dof_frag } from '../../mol-gl/shader/dof.frag';
import { Viewport } from '../camera/util';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { isTimingMode } from '../../mol-util/debug';
import { ICamera } from '../../mol-canvas3d/camera';
import { Sphere3D } from '../../mol-math/geometry';
import { PostprocessingProps } from './postprocessing';
export const DofParams = {
blurSize: PD.Numeric(9, { min: 1, max: 32, step: 1 }),
blurSpread: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
inFocus: PD.Numeric(0.0, { min: -5000.0, max: 5000.0, step: 1.0 }, { description: 'Distance from the scene center that will be in focus' }),
PPM: PD.Numeric(20.0, { min: 0.0, max: 5000.0, step: 0.1 }, { description: 'Size of the area that will be in focus' }),
center: PD.Select('camera-target', PD.arrayToOptions(['scene-center', 'camera-target'])),
mode: PD.Select('plane', PD.arrayToOptions(['plane', 'sphere'])),
};
export type DofProps = PD.Values<typeof DofParams>
export class DofPass {
static isEnabled(props: PostprocessingProps) {
return props.dof.name !== 'off';
}
readonly target: RenderTarget;
private readonly renderable: DofRenderable;
constructor(private webgl: WebGLContext, width: number, height: number) {
this.target = webgl.createRenderTarget(width, height, false);
const nullTexture = createNullTexture();
this.renderable = getDofRenderable(webgl, nullTexture, nullTexture, nullTexture);
}
private updateState(viewport: Viewport) {
const { gl, state } = this.webgl;
state.enable(gl.SCISSOR_TEST);
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
const { x, y, width, height } = viewport;
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
state.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
setSize(width: number, height: number) {
const w = this.target.texture.getWidth();
const h = this.target.texture.getHeight();
if (width !== w || height !== h) {
this.target.setSize(width, height);
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
}
}
update(camera: ICamera, input: Texture, depthOpaque: Texture, depthTransparent: Texture, props: DofProps, sphere: Sphere3D) {
let needsUpdate = false;
if (this.renderable.values.tColor.ref.value !== input) {
ValueCell.update(this.renderable.values.tColor, input);
needsUpdate = true;
}
if (this.renderable.values.tDepthOpaque.ref.value !== depthOpaque) {
ValueCell.update(this.renderable.values.tDepthOpaque, depthOpaque);
needsUpdate = true;
}
if (this.renderable.values.tDepthTransparent.ref.value !== depthTransparent) {
ValueCell.update(this.renderable.values.tDepthTransparent, depthTransparent);
needsUpdate = true;
}
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = this.renderable.values.uInvProjection.ref.value;
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.renderable.values.uTexSize.ref.value;
const v = camera.viewport;
ValueCell.update(this.renderable.values.uProjection, camera.projection);
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
ValueCell.update(this.renderable.values.uMode, props.mode === 'sphere' ? 1 : 0);
Vec4.set(this.renderable.values.uBounds.ref.value,
v.x / w,
v.y / h,
(v.x + v.width) / w,
(v.y + v.height) / h
);
ValueCell.update(this.renderable.values.uBounds, this.renderable.values.uBounds.ref.value);
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
const blurSize = Math.round(props.blurSize * this.webgl.pixelRatio);
if (this.renderable.values.dBlurSize.ref.value !== blurSize) {
ValueCell.update(this.renderable.values.dBlurSize, blurSize);
needsUpdate = true;
}
const wolrdCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
const distance = Vec3.distance(camera.state.position, wolrdCenter);
const inFocus = distance + props.inFocus;
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus);
// transform center in view space
const center = this.renderable.values.uCenter.ref.value;
Vec3.transformMat4(center, wolrdCenter, camera.view);
ValueCell.update(this.renderable.values.uCenter, center);
ValueCell.updateIfChanged(this.renderable.values.uBlurSpread, props.blurSpread);
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM);
if (needsUpdate) {
this.renderable.update();
}
}
render(viewport: Viewport, target: undefined | RenderTarget) {
if (isTimingMode) this.webgl.timer.mark('DofPass.render');
if (target) {
target.bind();
} else {
this.webgl.unbindFramebuffer();
}
this.updateState(viewport);
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('DofPass.render');
}
}
//
const DofSchema = {
...QuadSchema,
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uCenter: UniformSpec('v3'),
uMode: UniformSpec('i'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
dBlurSize: DefineSpec('number'),
uBlurSpread: UniformSpec('f'),
uInFocus: UniformSpec('f'),
uPPM: UniformSpec('f'),
};
const DofShaderCode = ShaderCode('dof', quad_vert, dof_frag);
type DofRenderable = ComputeRenderable<Values<typeof DofSchema>>
function getDofRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture): DofRenderable {
const width = colorTexture.getWidth();
const height = colorTexture.getHeight();
const values: Values<typeof DofSchema> = {
...QuadValues,
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tColor: ValueCell.create(colorTexture),
uTexSize: ValueCell.create(Vec2.create(width, height)),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uCenter: ValueCell.create(Vec3()),
uMode: ValueCell.create(0),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
dBlurSize: ValueCell.create(5),
uBlurSpread: ValueCell.create(300.0),
uInFocus: ValueCell.create(20.0),
uPPM: ValueCell.create(20.0),
};
const schema = { ...DofSchema };
const renderItem = createComputeRenderItem(ctx, 'triangles', DofShaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -24,6 +24,7 @@ import { MarkingPass, MarkingProps } from './marking';
import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
import { AssetManager } from '../../mol-util/assets';
import { DofPass } from './dof';
import { BloomPass } from './bloom';
type Props = {
@@ -63,6 +64,7 @@ export class DrawPass {
readonly postprocessing: PostprocessingPass;
readonly antialiasing: AntialiasingPass;
readonly bloom: BloomPass;
readonly dof: DofPass;
private transparencyMode: TransparencyMode = 'blended';
setTransparency(transparency: 'wboit' | 'dpoit' | 'blended') {
@@ -107,6 +109,7 @@ export class DrawPass {
this.postprocessing = new PostprocessingPass(webgl, assetManager, this);
this.antialiasing = new AntialiasingPass(webgl, width, height);
this.bloom = new BloomPass(webgl, width, height);
this.dof = new DofPass(webgl, width, height);
this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
this.copyFboPostprocessing = createCopyRenderable(webgl, this.postprocessing.target.texture);
@@ -148,6 +151,7 @@ export class DrawPass {
this.marking.setSize(width, height);
this.postprocessing.setSize(width, height);
this.antialiasing.setSize(width, height);
this.dof.setSize(width, height);
this.bloom.setSize(width, height);
}
@@ -162,15 +166,18 @@ export class DrawPass {
renderer.renderDpoitOpaque(scene.primitives, camera, null);
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
}
@@ -216,15 +223,18 @@ export class DrawPass {
renderer.renderWboitOpaque(scene.primitives, camera, null);
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
}
@@ -274,6 +284,17 @@ export class DrawPass {
this.colorTarget.bind();
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
if (!this.packedDepth) {
this.depthTextureOpaque.detachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
@@ -281,14 +302,6 @@ export class DrawPass {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
}
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
if (!this.packedDepth) {
@@ -330,6 +343,7 @@ export class DrawPass {
const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const dofEnabled = DofPass.isEnabled(props.postprocessing);
const { x, y, width, height } = camera.viewport;
renderer.setViewport(x, y, width, height);
@@ -388,12 +402,30 @@ export class DrawPass {
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
}
let needsTargetCopy = false;
if (antialiasingEnabled) {
const input = PostprocessingPass.isEnabled(props.postprocessing)
? this.postprocessing.target.texture
: this.colorTarget.texture;
this.antialiasing.render(camera, input, toDrawingBuffer, props.postprocessing);
} else if (toDrawingBuffer) {
this.antialiasing.render(camera, input, toDrawingBuffer && !dofEnabled, props.postprocessing);
} else if (toDrawingBuffer && !DofPass.isEnabled(props.postprocessing)) {
needsTargetCopy = true;
}
if (props.postprocessing.dof.name === 'on') {
const input = AntialiasingPass.isEnabled(props.postprocessing)
? this.antialiasing.target.texture
: PostprocessingPass.isEnabled(props.postprocessing)
? this.postprocessing.target.texture
: this.colorTarget.texture;
this.dof.update(camera, input, this.depthTargetOpaque?.texture || this.depthTextureOpaque, this.depthTextureTransparent, props.postprocessing.dof.params, scene.boundingSphereVisible);
this.dof.render(camera.viewport, toDrawingBuffer ? undefined : this.getColorTarget(props.postprocessing));
} else if (toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing)) {
needsTargetCopy = true;
}
if (needsTargetCopy) {
this.drawTarget.bind();
this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
@@ -448,7 +480,9 @@ export class DrawPass {
}
getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {
if (AntialiasingPass.isEnabled(postprocessingProps)) {
if (DofPass.isEnabled(postprocessingProps)) {
return this.dof.target;
} else if (AntialiasingPass.isEnabled(postprocessingProps)) {
return this.antialiasing.target;
} else if (PostprocessingPass.isEnabled(postprocessingProps)) {
return this.postprocessing.target;

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -54,6 +54,7 @@ export const MultiSampleParams = {
mode: PD.Select('temporal', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }, { description: 'Take level^2 samples.' }),
reduceFlicker: PD.Boolean(true, { description: 'Reduce flicker in "temporal" mode.' }),
reuseOcclusion: PD.Boolean(true, { description: 'Reuse occlusion data. It is faster but has some artefacts.' }),
};
export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
@@ -161,7 +162,7 @@ export class MultiSamplePass {
ValueCell.update(compose.values.uWeight, sampleWeight);
// render scene
if (i === 0) {
if (i === 0 || !props.multiSample.reuseOcclusion) {
drawPass.postprocessing.setOcclusionOffset(0, 0);
} else {
drawPass.postprocessing.setOcclusionOffset(
@@ -252,7 +253,7 @@ export class MultiSamplePass {
camera.update();
// render scene
if (sampleIndex === 0) {
if (sampleIndex === 0 || !props.multiSample.reuseOcclusion) {
drawPass.postprocessing.setOcclusionOffset(0, 0);
} else {
drawPass.postprocessing.setOcclusionOffset(

View File

@@ -0,0 +1,138 @@
/**
* Copyright (c) 2019-2024 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>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
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 { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { DrawPass } from './draw';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
import { isTimingMode } from '../../mol-util/debug';
import { Color } from '../../mol-util/color';
import { PostprocessingProps } from './postprocessing';
export const OutlineParams = {
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' }),
};
export type OutlineProps = PD.Values<typeof OutlineParams>
export class OutlinePass {
static isEnabled(props: PostprocessingProps) {
return props.outline.name !== 'off';
}
readonly outlinesTarget: RenderTarget;
private readonly outlinesRenderable: OutlinesRenderable;
constructor(private readonly webgl: WebGLContext, readonly drawPass: DrawPass, width: number, height: number) {
const { depthTextureTransparent, depthTextureOpaque } = drawPass;
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
}
setSize(width: number, height: number) {
const [w, h] = this.outlinesRenderable.values.uTexSize.ref.value;
if (width !== w || height !== h) {
this.outlinesTarget.setSize(width, height);
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
}
}
update(camera: ICamera, props: OutlineProps) {
let needsUpdateOutlines = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const transparentOutline = props.includeTransparent ?? true;
const outlineThreshold = 50 * props.threshold * this.webgl.pixelRatio;
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
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);
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
}
}
render() {
if (isTimingMode) this.webgl.timer.mark('OUTLINE.render');
this.outlinesTarget.bind();
this.outlinesRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('OUTLINE.render');
}
}
export const OutlinesSchema = {
...QuadSchema,
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
const values: Values<typeof OutlinesSchema> = {
...QuadValues,
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
uTexSize: ValueCell.create(Vec2.create(width, height)),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...OutlinesSchema };
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}

View File

@@ -4,27 +4,24 @@
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Ludovic Autin <ludovic.autin@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
import { deepEqual, ValueCell } from '../../mol-util';
import { 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';
import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { DrawPass } from './draw';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
import { Color } from '../../mol-util/color';
import { FxaaParams, FxaaPass } from './fxaa';
import { SmaaParams, SmaaPass } from './smaa';
@@ -32,261 +29,19 @@ 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';
import { CasParams, CasPass } from './cas';
import { DofParams } from './dof';
import { BloomParams } from './bloom';
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
export const OutlinesSchema = {
...QuadSchema,
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
dOrthographic: DefineSpec('number'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uOutlineThreshold: UniformSpec('f'),
dTransparentOutline: DefineSpec('boolean'),
};
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
const values: Values<typeof OutlinesSchema> = {
...QuadValues,
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
uTexSize: ValueCell.create(Vec2.create(width, height)),
dOrthographic: ValueCell.create(0),
uNear: ValueCell.create(1),
uFar: ValueCell.create(10000),
uInvProjection: ValueCell.create(Mat4.identity()),
uOutlineThreshold: ValueCell.create(0.33),
dTransparentOutline: ValueCell.create(transparentOutline),
};
const schema = { ...OutlinesSchema };
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
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'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uTexSize: UniformSpec('v2'),
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, 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),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
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 };
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoBlurSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uKernel: UniformSpec('f[]'),
dOcclusionKernelSize: DefineSpec('number'),
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
};
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
const values: Values<typeof SsaoBlurSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
uKernel: ValueCell.create(getBlurKernel(15)),
dOcclusionKernelSize: ValueCell.create(15),
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
};
const schema = { ...SsaoBlurSchema };
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
function getBlurKernel(kernelSize: number): number[] {
const sigma = kernelSize / 3.0;
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
const kernel = [];
for (let x = 0; x < halfKernelSize; x++) {
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
}
return kernel;
}
const RandomHemisphereVector: Vec3[] = [];
for (let i = 0; i < 256; i++) {
const v = Vec3();
v[0] = Math.random() * 2.0 - 1.0;
v[1] = Math.random() * 2.0 - 1.0;
v[2] = Math.random();
Vec3.normalize(v, v);
Vec3.scale(v, v, Math.random());
RandomHemisphereVector.push(v);
}
function getSamples(nSamples: number): number[] {
const samples = [];
for (let i = 0; i < nSamples; i++) {
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
scale = 0.1 + scale * (1.0 - 0.1);
samples.push(RandomHemisphereVector[i][0] * scale);
samples.push(RandomHemisphereVector[i][1] * scale);
samples.push(RandomHemisphereVector[i][2] * scale);
}
return samples;
}
const PostprocessingSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
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'),
@@ -312,13 +67,12 @@ const PostprocessingSchema = {
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: 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())),
@@ -352,50 +106,21 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
export const PostprocessingParams = {
occlusion: PD.MappedStatic('on', {
on: PD.Group({
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
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)),
}),
on: PD.Group(SsaoParams),
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 }),
}),
on: PD.Group(ShadowParams),
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' }),
}),
on: PD.Group(OutlineParams),
off: PD.Group({})
}, { cycle: true, description: 'Draw outline around 3D objects' }),
dof: PD.MappedStatic('off', {
on: PD.Group(DofParams),
off: PD.Group({})
}, { cycle: true, description: 'DOF' }),
antialiasing: PD.MappedStatic('smaa', {
fxaa: PD.Group(FxaaParams),
smaa: PD.Group(SmaaParams),
@@ -414,381 +139,84 @@ export const PostprocessingParams = {
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.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
}
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return props.outline.name === 'on' && props.outline.params.includeTransparent;
return OutlinePass.isEnabled(props) && (props.outline.params as OutlineProps).includeTransparent;
}
readonly target: RenderTarget;
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;
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;
private readonly ssaoRenderable: SsaoRenderable;
private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
private nSamples: number;
private blurKernelSize: number;
private readonly renderable: PostprocessingRenderable;
private ssaoScale: number;
private calcSsaoScale(resolutionScale: number) {
// downscale ssao for high pixel-ratios
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
}
private levels: { radius: number, bias: number }[];
readonly ssao: SsaoPass;
readonly shadow: ShadowPass;
readonly outline: OutlinePass;
private readonly bgColor = Vec3();
readonly background: BackgroundPass;
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) {
const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, readonly drawPass: DrawPass) {
const { colorTarget, depthTextureOpaque } = drawPass;
const width = colorTarget.getWidth();
const height = colorTarget.getHeight();
this.nSamples = 1;
this.blurKernelSize = 1;
this.ssaoScale = this.calcSsaoScale(1);
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, true);
this.ssao = new SsaoPass(webgl, drawPass, width, height);
this.shadow = new ShadowPass(webgl, drawPass, width, height);
this.outline = new OutlinePass(webgl, drawPass, width, height);
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
this.ssaoFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
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', 'nearest', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
const depthTexture = this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture;
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'nearest', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'nearest', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.ssaoDepthBlurProxyTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture, 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.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, this.shadow.shadowsTarget.texture, this.outline.outlinesTarget.texture, this.ssao.ssaoDepthTexture, true);
this.background = new BackgroundPass(webgl, assetManager, width, height);
}
setSize(width: number, height: number) {
const [w, h] = this.renderable.values.uTexSize.ref.value;
const ssaoScale = this.calcSsaoScale(1);
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.ssaoScale = ssaoScale;
if (width !== w || height !== h) {
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));
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
this.depthHalfRenderable.update();
this.ssaoRenderable.update();
this.background.setSize(width, height);
}
this.ssao.setSize(width, height);
this.shadow.setSize(width, height);
this.outline.setSize(width, height);
this.background.setSize(width, height);
}
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
let needsUpdateShadows = false;
let needsUpdateMain = false;
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = 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 outlinesEnabled = OutlinePass.isEnabled(props);
const shadowsEnabled = ShadowPass.isEnabled(props);
const occlusionEnabled = SsaoPass.isEnabled(props);
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 b = this.ssaoRenderable.values.uBounds;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
Math.floor(v.y * s) / (h * s),
Math.ceil((v.x + v.width) * s) / (w * s),
Math.ceil((v.y + v.height) * s) / (h * s)
);
ValueCell.update(b, b.ref.value);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
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);
}
if (this.nSamples !== props.occlusion.params.samples) {
needsUpdateSsao = true;
this.nSamples = props.occlusion.params.samples;
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
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) {
needsUpdateSsaoBlur = true;
this.blurKernelSize = props.occlusion.params.blurKernelSize;
const kernel = getBlurKernel(this.blurKernelSize);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
if (this.ssaoScale !== ssaoScale) {
needsUpdateSsao = true;
needsUpdateDepthHalf = true;
this.ssaoScale = ssaoScale;
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);
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
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 (occlusionEnabled) {
this.ssao.update(camera, props.occlusion.params as SsaoProps);
}
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 (shadowsEnabled) {
this.shadow.update(camera, light, props.shadow.params as ShadowProps);
}
if (props.outline.name === 'on') {
const transparentOutline = props.outline.params.includeTransparent ?? true;
const outlineScale = Math.max(1, Math.round(props.outline.params.scale * this.webgl.pixelRatio)) - 1;
const outlineThreshold = 50 * props.outline.params.threshold * this.webgl.pixelRatio;
if (outlinesEnabled) {
const outlineProps = props.outline.params as OutlineProps;
this.outline.update(camera, outlineProps);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
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);
const transparentOutline = outlineProps.includeTransparent ?? true;
const outlineScale = Math.max(1, Math.round(outlineProps.scale * this.webgl.pixelRatio)) - 1;
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, outlineProps.color));
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
needsUpdateMain = true;
@@ -806,6 +234,7 @@ 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.update(this.renderable.values.dOrthographic, orthographic);
@@ -824,27 +253,6 @@ export class PostprocessingPass {
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
if (needsUpdateOutlines) {
this.outlinesRenderable.update();
}
if (needsUpdateShadows) {
this.shadowsRenderable.update();
}
if (needsUpdateSsao) {
this.ssaoRenderable.update();
}
if (needsUpdateSsaoBlur) {
this.ssaoBlurFirstPassRenderable.update();
this.ssaoBlurSecondPassRenderable.update();
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
}
if (needsUpdateMain) {
this.renderable.update();
}
@@ -879,54 +287,18 @@ export class PostprocessingPass {
// 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();
this.ssaoBlurFirstPassFramebuffer.bind();
this.ssaoBlurFirstPassRenderable.render();
this.ssaoBlurSecondPassFramebuffer.bind();
this.ssaoBlurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
this.ssao.render(camera);
}
state.viewport(x, y, width, height);
state.scissor(x, y, width, height);
if (props.outline.name === 'on') {
this.outlinesTarget.bind();
this.outlinesRenderable.render();
this.outline.render();
}
if (props.shadow.name === 'on') {
this.shadowsTarget.bind();
this.shadowsRenderable.render();
this.shadow.render();
}
if (toDrawingBuffer) {

View File

@@ -0,0 +1,175 @@
/**
* Copyright (c) 2019-2024 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>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
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 { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
import { Mat4, Vec2, Vec4 } from '../../mol-math/linear-algebra';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { DrawPass } from './draw';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { isTimingMode } from '../../mol-util/debug';
import { Light } from '../../mol-gl/renderer';
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
import { PostprocessingProps } from './postprocessing';
export const ShadowParams = {
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 }),
};
export type ShadowProps = PD.Values<typeof ShadowParams>
export class ShadowPass {
static isEnabled(props: PostprocessingProps) {
return props.shadow.name !== 'off';
}
readonly shadowsTarget: RenderTarget;
private readonly shadowsRenderable: ShadowsRenderable;
constructor(readonly webgl: WebGLContext, readonly drawPass: DrawPass, width: number, height: number) {
const { depthTextureOpaque } = drawPass;
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
}
setSize(width: number, height: number) {
const [w, h] = this.shadowsRenderable.values.uTexSize.ref.value;
if (width !== w || height !== h) {
this.shadowsTarget.setSize(width, height);
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
}
}
update(camera: ICamera, light: Light, props: ShadowProps) {
let needsUpdateShadows = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.shadowsRenderable.values.uTexSize.ref.value;
const v = camera.viewport;
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.maxDistance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.tolerance);
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.bias);
if (this.shadowsRenderable.values.dSteps.ref.value !== props.steps) {
ValueCell.update(this.shadowsRenderable.values.dSteps, props.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 (needsUpdateShadows) {
this.shadowsRenderable.update();
}
}
render() {
if (isTimingMode) this.webgl.timer.mark('SHADOW.render');
this.shadowsTarget.bind();
this.shadowsRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SHADOW.render');
}
}
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);
}

View File

@@ -0,0 +1,541 @@
/**
* Copyright (c) 2019-2024 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>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
import { ShaderCode } from '../../mol-gl/shader-code';
import { WebGLContext } from '../../mol-gl/webgl/context';
import { Texture } from '../../mol-gl/webgl/texture';
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';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
import { RenderTarget } from '../../mol-gl/webgl/render-target';
import { DrawPass } from './draw';
import { ICamera } from '../../mol-canvas3d/camera';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
import { Color } from '../../mol-util/color';
import { isTimingMode } from '../../mol-util/debug';
import { PostprocessingProps } from './postprocessing';
export const SsaoParams = {
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
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 }),
blurDepthBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
color: PD.Color(Color(0x000000)),
};
export type SsaoProps = PD.Values<typeof SsaoParams>
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 SsaoPass {
static isEnabled(props: PostprocessingProps) {
return props.occlusion.name !== 'off';
}
readonly target: RenderTarget;
private readonly ssaoFramebuffer: Framebuffer;
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
readonly ssaoDepthTexture: Texture;
private readonly ssaoDepthBlurProxyTexture: Texture;
private readonly ssaoRenderable: SsaoRenderable;
private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
private nSamples: number;
private blurKernelSize: number;
private texSize: [number, number];
private ssaoScale: number;
private calcSsaoScale(resolutionScale: number) {
// downscale ssao for high pixel-ratios
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
}
private levels: { radius: number, bias: number }[];
constructor(private readonly webgl: WebGLContext, private readonly drawPass: DrawPass, width: number, height: number) {
const { textureFloatLinear } = webgl.extensions;
const { depthTextureOpaque } = drawPass;
this.nSamples = 1;
this.blurKernelSize = 1;
this.ssaoScale = this.calcSsaoScale(1);
this.texSize = [width, height];
this.levels = [];
this.ssaoFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
const sw = Math.floor(width * this.ssaoScale);
const sh = Math.floor(height * this.ssaoScale);
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));
const filter = textureFloatLinear ? 'linear' : 'nearest';
this.downsampledDepthTarget = drawPass.packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', filter, 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
const depthTexture = this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture;
this.depthHalfTarget = drawPass.packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', filter, 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
this.depthQuarterTarget = drawPass.packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', filter, 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', filter);
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', filter);
this.ssaoDepthBlurProxyTexture.define(sw, sh);
this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
}
setSize(width: number, height: number) {
const [w, h] = this.texSize;
const ssaoScale = this.calcSsaoScale(1);
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
this.texSize.splice(0, 2, 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.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));
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
this.depthHalfRenderable.update();
this.ssaoRenderable.update();
}
}
update(camera: ICamera, props: SsaoProps) {
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const invProjection = Mat4.identity();
Mat4.invert(invProjection, camera.projection);
const [w, h] = this.texSize;
const v = camera.viewport;
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
const b = this.ssaoRenderable.values.uBounds;
const s = this.ssaoScale;
Vec4.set(b.ref.value,
Math.floor(v.x * s) / (w * s),
Math.floor(v.y * s) / (h * s),
Math.ceil((v.x + v.width) * s) / (w * s),
Math.ceil((v.y + v.height) * s) / (h * s)
);
ValueCell.update(b, b.ref.value);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
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);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
needsUpdateSsaoBlur = true;
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
}
if (this.nSamples !== props.samples) {
needsUpdateSsao = true;
this.nSamples = props.samples;
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
}
const multiScale = props.multiScale.name === 'on';
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
}
if (props.multiScale.name === 'on') {
const mp = props.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.radius));
}
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.bias);
if (this.blurKernelSize !== props.blurKernelSize) {
needsUpdateSsaoBlur = true;
this.blurKernelSize = props.blurKernelSize;
const kernel = getBlurKernel(this.blurKernelSize);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
}
const ssaoScale = this.calcSsaoScale(props.resolutionScale);
if (this.ssaoScale !== ssaoScale) {
needsUpdateSsao = true;
needsUpdateDepthHalf = true;
this.ssaoScale = ssaoScale;
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);
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
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));
}
if (needsUpdateSsao) {
this.ssaoRenderable.update();
}
if (needsUpdateSsaoBlur) {
this.ssaoBlurFirstPassRenderable.update();
this.ssaoBlurSecondPassRenderable.update();
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
}
}
render(camera: ICamera) {
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const { state } = this.webgl;
const { x, y, width, height } = camera.viewport;
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();
this.ssaoBlurFirstPassFramebuffer.bind();
this.ssaoBlurFirstPassRenderable.render();
this.ssaoBlurSecondPassFramebuffer.bind();
this.ssaoBlurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
}
}
const SsaoSchema = {
...QuadSchema,
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
uProjection: UniformSpec('m4'),
uInvProjection: UniformSpec('m4'),
uBounds: UniformSpec('v4'),
uTexSize: UniformSpec('v2'),
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, 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),
uProjection: ValueCell.create(Mat4.identity()),
uInvProjection: ValueCell.create(Mat4.identity()),
uBounds: ValueCell.create(Vec4()),
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
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 };
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
const SsaoBlurSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
uTexSize: UniformSpec('v2'),
uKernel: UniformSpec('f[]'),
dOcclusionKernelSize: DefineSpec('number'),
uBlurDepthBias: UniformSpec('f'),
uBlurDirectionX: UniformSpec('f'),
uBlurDirectionY: UniformSpec('f'),
uInvProjection: UniformSpec('m4'),
uNear: UniformSpec('f'),
uFar: UniformSpec('f'),
uBounds: UniformSpec('v4'),
dOrthographic: DefineSpec('number'),
};
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
const values: Values<typeof SsaoBlurSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
uKernel: ValueCell.create(getBlurKernel(15)),
dOcclusionKernelSize: ValueCell.create(15),
uBlurDepthBias: ValueCell.create(0.5),
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
uInvProjection: ValueCell.create(Mat4.identity()),
uNear: ValueCell.create(0.0),
uFar: ValueCell.create(10000.0),
uBounds: ValueCell.create(Vec4()),
dOrthographic: ValueCell.create(0),
};
const schema = { ...SsaoBlurSchema };
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
return createComputeRenderable(renderItem, values);
}
function getBlurKernel(kernelSize: number): number[] {
const sigma = kernelSize / 3.0;
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
const kernel = [];
for (let x = 0; x < halfKernelSize; x++) {
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
}
return kernel;
}
const RandomHemisphereVector: Vec3[] = [];
for (let i = 0; i < 256; i++) {
const v = Vec3();
v[0] = Math.random() * 2.0 - 1.0;
v[1] = Math.random() * 2.0 - 1.0;
v[2] = Math.random();
Vec3.normalize(v, v);
Vec3.scale(v, v, Math.random());
RandomHemisphereVector.push(v);
}
function getSamples(nSamples: number): number[] {
const samples = [];
for (let i = 0; i < nSamples; i++) {
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
scale = 0.1 + scale * (1.0 - 0.1);
samples.push(RandomHemisphereVector[i][0] * scale);
samples.push(RandomHemisphereVector[i][1] * scale);
samples.push(RandomHemisphereVector[i][2] * scale);
}
return samples;
}

View File

@@ -168,6 +168,7 @@ export namespace Cylinders {
sizeAspectRatio: PD.Numeric(1, { min: 0, max: 3, step: 0.01 }),
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
@@ -259,6 +260,7 @@ export namespace Cylinders {
uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
uDoubleSided: ValueCell.create(props.doubleSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
dSolidInterior: ValueCell.create(props.solidInterior),
@@ -279,6 +281,7 @@ export namespace Cylinders {
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor * props.sizeAspectRatio);
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);

View File

@@ -148,6 +148,7 @@ export namespace DirectVolume {
export const Params = {
...BaseGeometry.Params,
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
controlPoints: PD.LineGraph([
Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
@@ -272,6 +273,7 @@ export namespace DirectVolume {
dAxisOrder: ValueCell.create(directVolume.axisOrder.ref.value.join('')),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
};
}
@@ -285,6 +287,7 @@ export namespace DirectVolume {
function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
BaseGeometry.updateValues(values, props);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);

View File

@@ -628,6 +628,7 @@ export namespace Mesh {
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
@@ -713,6 +714,7 @@ export namespace Mesh {
dFlatShaded: ValueCell.create(props.flatShaded),
dFlipSided: ValueCell.create(props.flipSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
@@ -734,6 +736,7 @@ export namespace Mesh {
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);

View File

@@ -247,6 +247,7 @@ export namespace Spheres {
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
@@ -346,6 +347,7 @@ export namespace Spheres {
uSizeFactor: spheres.shaderData.sizeFactor,
uDoubleSided: ValueCell.create(props.doubleSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
dSolidInterior: ValueCell.create(props.solidInterior),
@@ -372,6 +374,7 @@ export namespace Spheres {
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);

View File

@@ -121,6 +121,7 @@ export namespace TextureMesh {
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
@@ -226,6 +227,7 @@ export namespace TextureMesh {
dFlatShaded: ValueCell.create(props.flatShaded),
dFlipSided: ValueCell.create(props.flipSided),
dIgnoreLight: ValueCell.create(props.ignoreLight),
dCelShaded: ValueCell.create(props.celShaded),
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
uBumpFrequency: ValueCell.create(props.bumpFrequency),
@@ -247,6 +249,7 @@ export namespace TextureMesh {
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);

View File

@@ -26,6 +26,7 @@ export const CylindersSchema = {
padding: ValueSpec('number'),
uDoubleSided: UniformSpec('b', 'material'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
dSolidInterior: DefineSpec('boolean'),

View File

@@ -40,6 +40,7 @@ export const DirectVolumeSchema = {
dAxisOrder: DefineSpec('string', ['012', '021', '102', '120', '201', '210']),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
};
export type DirectVolumeSchema = typeof DirectVolumeSchema

View File

@@ -22,6 +22,7 @@ export const MeshSchema = {
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),

View File

@@ -169,6 +169,7 @@ export const GlobalUniformSchema = {
uMarkerAverage: UniformSpec('f'),
uXrayEdgeFalloff: UniformSpec('f'),
uCelSteps: UniformSpec('f'),
uExposure: UniformSpec('f'),
uRenderMask: UniformSpec('i'),

View File

@@ -21,6 +21,7 @@ export const SpheresSchema = {
padding: ValueSpec('number'),
uDoubleSided: UniformSpec('b', 'material'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
dSolidInterior: DefineSpec('boolean'),

View File

@@ -22,6 +22,7 @@ export const TextureMeshSchema = {
uDoubleSided: UniformSpec('b', 'material'),
dFlipSided: DefineSpec('boolean'),
dIgnoreLight: DefineSpec('boolean'),
dCelShaded: DefineSpec('boolean'),
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
uBumpFrequency: UniformSpec('f', 'material'),

View File

@@ -109,6 +109,7 @@ export const RendererParams = {
markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]),
xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
celSteps: PD.Numeric(5, { min: 2, max: 16, step: 1 }),
exposure: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.01 }),
light: PD.ObjectList({
@@ -258,6 +259,7 @@ namespace Renderer {
uMarkerAverage: ValueCell.create(0),
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
uCelSteps: ValueCell.create(p.celSteps),
uExposure: ValueCell.create(p.exposure),
};
const globalUniformList = Object.entries(globalUniforms);
@@ -853,6 +855,12 @@ namespace Renderer {
p.xrayEdgeFalloff = props.xrayEdgeFalloff;
ValueCell.update(globalUniforms.uXrayEdgeFalloff, p.xrayEdgeFalloff);
}
if (props.celSteps !== undefined && props.celSteps !== p.celSteps) {
p.celSteps = props.celSteps;
ValueCell.update(globalUniforms.uCelSteps, p.celSteps);
}
if (props.exposure !== undefined && props.exposure !== p.exposure) {
p.exposure = props.exposure;
ValueCell.update(globalUniforms.uExposure, p.exposure);

View File

@@ -173,7 +173,7 @@ function ignoreDefine(name: string, variant: string, defines: ShaderDefines): bo
'dLightCount', 'dXrayShaded',
'dOverpaintType', 'dOverpaint',
'dSubstanceType', 'dSubstance',
'dColorMarker',
'dColorMarker', 'dCelShaded'
];
if (variant !== 'emissive') {
ignore.push('dEmissiveType', 'dEmissive');

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*
* adapted from three.js (https://github.com/mrdoob/three.js/)
* which under the MIT License, Copyright © 2010-2021 three.js authors
@@ -30,7 +31,16 @@ export const apply_light_color = `
vec4 color = material;
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#if defined(dCelShaded)
// clamp to avoid artifacts
metalness = clamp(metalness, 0.0, 0.99);
roughness = clamp(roughness, 0.05, 1.0);
#endif
GeometricContext geometry;
geometry.position = -vViewPosition;
geometry.normal = normal;
geometry.viewDir = normalize(vViewPosition);
PhysicalMaterial physicalMaterial;
physicalMaterial.diffuseColor = color.rgb * (1.0 - metalness);
@@ -44,30 +54,53 @@ export const apply_light_color = `
physicalMaterial.specularColor = mix(vec3(0.04), color.rgb, metalness);
physicalMaterial.specularF90 = 1.0;
GeometricContext geometry;
geometry.position = -vViewPosition;
geometry.normal = normal;
geometry.viewDir = normalize(vViewPosition);
IncidentLight directLight;
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
}
#pragma unroll_loop_end
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
vec3 outgoingLight = vec3(0.0);
// indirect specular only metals
vec3 radiance = uAmbientColor * metalness;
vec3 iblIrradiance = uAmbientColor * metalness;
vec3 clearcoatRadiance = vec3(0.0);
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
#if defined(dCelShaded)
float celDiffuse;
float celSpecular;
float celIntensity;
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
celDiffuse = RECIPROCAL_PI * max(dot(geometry.normal, directLight.direction), 0.0) * (1.0 - metalness);
celSpecular = luminance(saturate(dot(geometry.normal, directLight.direction)) * BRDF_GGX(directLight.direction, geometry.viewDir, geometry.normal, physicalMaterial.specularColor, physicalMaterial.specularF90, roughness));
celIntensity = celDiffuse + celSpecular;
celIntensity = ceil(celIntensity * uCelSteps) / uCelSteps;
outgoingLight += color.rgb * directLight.color * celIntensity;
}
#pragma unroll_loop_end
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
#else
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
#pragma unroll_loop_start
for (int i = 0; i < dLightCount; ++i) {
directLight.direction = uLightDirection[i];
directLight.color = uLightColor[i] * PI; // * PI for punctual light
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
}
#pragma unroll_loop_end
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
// indirect specular only metals
vec3 radiance = uAmbientColor * metalness;
vec3 iblIrradiance = uAmbientColor * metalness;
vec3 clearcoatRadiance = vec3(0.0);
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
#endif
outgoingLight = clamp(outgoingLight, 0.01, 0.99); // prevents black artifacts on specular highlight with transparent background
#if defined(dRenderVariant_color)

View File

@@ -76,6 +76,7 @@ uniform vec3 uInteriorColor;
bool interior;
uniform float uXrayEdgeFalloff;
uniform float uCelSteps;
uniform float uExposure;
uniform mat4 uProjection;

View File

@@ -103,6 +103,12 @@ vec4 linearTosRGB(const in vec4 c) {
return vec4(mix(pow(c.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), c.rgb * 12.92, vec3(lessThanEqual(c.rgb, vec3(0.0031308)))), c.a);
}
float luminance(vec3 c) {
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
return dot(c, W);
}
float linearizeDepth(const in float depth, const in float near, const in float far) {
return (2.0 * near) / (far + near - depth * (far - near));
}

View File

@@ -2,6 +2,7 @@
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
export const cylinders_frag = `
@@ -120,7 +121,11 @@ bool CylinderImpostor(
#ifdef dSolidInterior
if (interior) cameraNormal = -rayDir;
#endif
return true;
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
return true;
#else
return !interior;
#endif
}
}
} else if (bottomCap && y >= 0.0) {
@@ -146,7 +151,11 @@ bool CylinderImpostor(
#ifdef dSolidInterior
if (interior) cameraNormal = -rayDir;
#endif
return true;
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
return true;
#else
return !interior;
#endif
}
}
}

View File

@@ -77,6 +77,7 @@ uniform vec3 uFogColor;
uniform float uAlpha;
uniform bool uTransparentBackground;
uniform float uXrayEdgeFalloff;
uniform float uCelSteps;
uniform float uExposure;
uniform int uRenderMask;

View File

@@ -0,0 +1,120 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Ludovic Autin <autin@scripps.edu>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
export const dof_frag = `
precision highp float;
precision highp int;
precision highp sampler2D;
#include common
uniform sampler2D tColor;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform vec2 uTexSize;
uniform vec4 uBounds;
uniform float uBlurSpread;
uniform float uInFocus;
uniform float uPPM;
uniform float uNear; // Near plane
uniform float uFar; // Far plane
uniform mat4 uInvProjection; // Inverse projection
uniform mat4 uProjection; // projection
uniform int uMode; // 0-planar, 1-spherical
uniform vec3 uCenter; // Center of focus sphere in view space
// Function to convert depth value from depth buffer to view space Z
float getViewZ(const in float depth) {
#if dOrthographic == 1
return orthographicDepthToViewZ(depth, uNear, uFar);
#else
return perspectiveDepthToViewZ(depth, uNear, uFar);
#endif
}
// Retrieve depth from opaque depth texture
float getDepthOpaque(const in vec2 coords) {
#ifdef depthTextureSupport
return texture2D(tDepthOpaque, coords).r;
#else
return unpackRGBAToDepth(texture2D(tDepthOpaque, coords));
#endif
}
// Retrieve depth from transparent depth texture
float getDepthTransparent(const in vec2 coords) {
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
}
bool isBackground(const in float depth) {
return depth == 1.0;
}
float getDepth(const in vec2 coords) {
return min(getDepthOpaque(coords), getDepthTransparent(coords));
}
float getCOC(vec2 uv) {
float depth = getDepth(uv);
float viewDist = getViewZ(depth);
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
float focusDist = length(aposition - uCenter);
float coc = 0.0; // Circle of Confusion
if (uMode == 0) { // planar Depth of field
coc = (abs(viewDist) - uInFocus) / uPPM; //focus distance, focus range
} else if(uMode == 1) { // spherical Depth of field
coc = focusDist / uPPM ;
}
coc = clamp(coc, -1.0, 1.0);
return coc;
}
// Simple box blur for blurring the image
vec3 getBlurredImage(vec2 coords) {
vec4 blurColor = vec4(0);
vec2 texelSize = vec2(1.0 / uTexSize.x, 1.0 / uTexSize.y);
float count = 0.0;
for (int x = 0; x < int(dBlurSize); x++) {
for (int y = 0; y < int(dBlurSize); y++) {
vec2 offset = vec2(float(x) - float(dBlurSize) / 2.0, float(y) - float(dBlurSize) / 2.0);
vec2 uvPixel = coords.xy + offset * texelSize * uBlurSpread;
float coc = getCOC(uvPixel);
coc = smoothstep(0.0, 1.0, abs(coc));
// mix blurColor with new color with weight coc
blurColor.rgb = blurColor.rgb + texture2D(tColor, uvPixel).xyz * coc;
count+=coc;
}
}
blurColor = blurColor / count;
return blurColor.rgb;
}
// simplification from https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/
void main() {
vec2 uv = gl_FragCoord.xy / uTexSize;
vec4 color = texture2D(tColor, uv);
float depth = getDepth(uv);
float viewDist = getViewZ(depth);
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
float focusDist = length(aposition - uCenter);
vec3 blurColor = getBlurredImage(uv);
float coc = getCOC(uv); // Circle of Confusion
// for debugging the coc
// color.rgb = (coc < 0.0) ? (1.0 - abs(coc)) * vec3(1.0,0.0,0.0) : vec3(0.0, 1.0 - coc, 0.0) ;//mix(color.rgb, blurColor.rgb, abs(coc));
color.rgb = mix(color.rgb, blurColor, smoothstep(0.0, 1.0, abs(coc))); // Smooth blending based on CoC
gl_FragColor = color;
}
`;

View File

@@ -13,7 +13,6 @@ precision highp sampler2D;
uniform sampler2D tSsaoDepth;
uniform sampler2D tColor;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform sampler2D tShadows;
uniform sampler2D tOutlines;
uniform vec2 uTexSize;
@@ -46,14 +45,6 @@ float getDepthOpaque(const in vec2 coords) {
#endif
}
float getDepthTransparent(const in vec2 coords) {
#ifdef dTransparentOutline
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
#else
return 1.0;
#endif
}
bool isBackground(const in float depth) {
return depth == 1.0;
}
@@ -62,11 +53,6 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
float backgroundViewZ = 2.0 * uFar;
vec2 invTexSize = 1.0 / uTexSize;
float transparentDepth = getDepthTransparent(coords);
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
float selfDepth = min(opaqueDepth, transparentDepth);
float outline = 1.0;
closestTexel = 1.0;
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
@@ -80,9 +66,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
float sampleOutline = sampleOutlineCombined.r;
float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
outline = 0.0;
closestTexel = sampleOutlineDepth;

View File

@@ -16,6 +16,7 @@ uniform vec2 uTexSize;
uniform vec4 uBounds;
uniform float uKernel[dOcclusionKernelSize];
uniform float uBlurDepthBias;
uniform float uBlurDirectionX;
uniform float uBlurDirectionY;
@@ -38,6 +39,10 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
bool isNearClip(const in float depth) {
return depth == 0.0;
}
bool outsideBounds(const in vec2 p) {
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
}
@@ -59,16 +64,14 @@ void main(void) {
}
float selfDepth = unpackRGToUnitInterval(packedDepth);
// if background and if second pass
if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
// (if background and if second pass) or if near clip
if ((isBackground(selfDepth) && uBlurDirectionY != 0.0) || isNearClip(selfDepth)) {
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
return;
}
float selfViewZ = getViewZ(selfDepth);
float pixelSize = getPixelSize(coords, selfDepth);
// max diff depth between two pixels
float maxDiffViewZ = 1.0;
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
@@ -86,12 +89,12 @@ void main(void) {
vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
float sampleDepth = unpackRGToUnitInterval(sampleSsaoDepth.zw);
if (isBackground(sampleDepth)) {
if (isBackground(sampleDepth) || isNearClip(sampleDepth)) {
continue;
}
float sampleViewZ = getViewZ(sampleDepth);
if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
if (abs(selfViewZ - sampleViewZ) >= uBlurDepthBias) {
continue;
}

View File

@@ -344,6 +344,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
contextRestored,
setContextLost: () => {
isContextLost = true;
timer.clear();
},
handleContextRestored: (extraResets?: () => void) => {
Object.assign(extensions, createExtensions(gl));

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -33,6 +33,10 @@ class MovingAverage {
return Object.fromEntries(this.avgs.entries());
}
clear() {
this.avgs.clear();
}
constructor(private count: number) { }
}
@@ -111,8 +115,13 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions,
dtq.deleteQuery(query);
});
pending.clear();
stack.length = 0;
gpuAvgs.clear();
cpuAvgs.clear();
measures = [];
current = null;
capturingStats = false;
};
const add = () => {

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.395, IHM 1.26, MA 1.4.6.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.395, IHM 1.26, MA 1.4.6.
*
* @author molstar/ciftools package
*/

View File

@@ -1,7 +1,7 @@
/**
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.395, IHM 1.26, MA 1.4.6.
*
* @author molstar/ciftools package
*/
@@ -290,7 +290,7 @@ export const mmCIF_Schema = {
/**
* Pointer to _atom_site.auth_seq_id
*/
pdbx_auth_seq_id: str,
pdbx_auth_seq_id: int,
/**
* Pointer to _atom_site.auth_asym_id
*/
@@ -793,6 +793,14 @@ export const mmCIF_Schema = {
* the entry.
*/
pdbx_number_of_molecules: int,
/**
* An identifier for the parent entity if this entity
* is part of a complex entity. For instance a chimeric
* entity may be decomposed into several independent
* chemical entities where each component entity was
* obtained from a different source.
*/
pdbx_parent_entity_id: str,
/**
* Details about any entity mutation(s).
*/
@@ -1483,6 +1491,116 @@ export const mmCIF_Schema = {
*/
pdbx_keywords: str,
},
/**
* Data items in the STRUCT_MON_PROT_CIS category identify
* monomers that have been found to have the peptide bond in the cis
* conformation. The criterion used to select residues to be
* designated as containing cis peptide bonds is given in
* _struct_mon_details.prot_cis.
*/
struct_mon_prot_cis: {
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_sites_alt.id in the
* ATOM_SITES_ALT category.
*/
label_alt_id: str,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.label_asym_id in the
* ATOM_SITE category.
*/
label_asym_id: str,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.label_comp_id in the
* ATOM_SITE category.
*/
label_comp_id: str,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.label_seq_id in the
* ATOM_SITE category.
*/
label_seq_id: int,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.auth_asym_id in the
* ATOM_SITE category.
*/
auth_asym_id: str,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.auth_comp_id in the
* ATOM_SITE category.
*/
auth_comp_id: str,
/**
* A component of the identifier for the monomer.
*
* This data item is a pointer to _atom_site.auth_seq_id in the
* ATOM_SITE category.
*/
auth_seq_id: int,
/**
* Pointer to _atom_site.auth_asym_id.
*/
pdbx_auth_asym_id_2: str,
/**
* Pointer to _atom_site.auth_comp_id.
*/
pdbx_auth_comp_id_2: str,
/**
* Pointer to _atom_site.auth_seq_id
*/
pdbx_auth_seq_id_2: int,
/**
* Pointer to _atom_site.label_asym_id.
*/
pdbx_label_asym_id_2: str,
/**
* Pointer to _atom_site.label_comp_id.
*/
pdbx_label_comp_id_2: str,
/**
* Pointer to _atom_site.label_seq_id
*/
pdbx_label_seq_id_2: int,
/**
* Pointer to _atom_site.pdbx_PDB_ins_code
*/
pdbx_PDB_ins_code: str,
/**
* Pointer to _atom_site.pdbx_PDB_ins_code
*/
pdbx_PDB_ins_code_2: str,
/**
* Pointer to _atom_site.pdbx_PDB_model_num
*/
pdbx_PDB_model_num: int,
/**
* omega torsion angle
*/
pdbx_omega_angle: str,
/**
* ordinal index
*/
pdbx_id: str,
/**
* PDB Insertion code
*/
pdbx_auth_ins_code: str,
/**
* PDB Insertion code
*/
pdbx_auth_ins_code_2: str,
},
/**
* Data items in the STRUCT_NCS_OPER category describe the
* noncrystallographic symmetry operations.
@@ -1699,7 +1817,7 @@ export const mmCIF_Schema = {
* This data item is a pointer to _atom_site.auth_seq_id in the
* ATOM_SITE category.
*/
pdbx_auth_seq_id: str,
pdbx_auth_seq_id: int,
/**
* PDB insertion code for the ligand in the site.
*/
@@ -1766,6 +1884,13 @@ export const mmCIF_Schema = {
* ATOM_SITE category.
*/
auth_asym_id: str,
/**
* A component of the identifier for participants in the site.
*
* This data item is a pointer to _atom_site.auth_atom_id in the
* ATOM_SITE category.
*/
auth_atom_id: str,
/**
* A component of the identifier for participants in the site.
*
@@ -1779,7 +1904,7 @@ export const mmCIF_Schema = {
* This data item is a pointer to _atom_site.auth_seq_id in the
* ATOM_SITE category.
*/
auth_seq_id: str,
auth_seq_id: int,
/**
* This data item is a pointer to _struct_site.id in the STRUCT_SITE
* category.
@@ -1800,6 +1925,21 @@ export const mmCIF_Schema = {
*/
pdbx_num_res: int,
},
/**
* Data items in the STRUCT_SITE_KEYWORDS category record
* keywords describing the site.
*/
struct_site_keywords: {
/**
* This data item is a pointer to _struct_site.id in the STRUCT_SITE
* category.
*/
site_id: str,
/**
* Keywords describing this site.
*/
text: str,
},
/**
* Data items in the SYMMETRY category record details about the
* space-group symmetry.
@@ -2082,7 +2222,7 @@ export const mmCIF_Schema = {
* This data item is a pointer to _atom_site.auth_seq_id in the
* ATOM_SITE category.
*/
auth_seq_id: str,
auth_seq_id: int,
/**
* Part of the identifier for the unobserved or zero occupancy residue.
*
@@ -3508,7 +3648,7 @@ export const mmCIF_Schema = {
},
/**
* Data items in the IHM_MODEL_LIST category record the
* details of the models being deposited.
* details of the structure models being deposited.
*/
ihm_model_list: {
/**
@@ -3540,7 +3680,7 @@ export const mmCIF_Schema = {
},
/**
* IHM_MODEL_GROUP category defines collections or groups of integrative
* structural models.
* structure models.
*/
ihm_model_group: {
/**
@@ -3568,8 +3708,8 @@ export const mmCIF_Schema = {
details: str,
},
/**
* IHM_MODEL_GROUP_LINK category provides the list of models present in
* a particular model group.
* IHM_MODEL_GROUP_LINK category provides the list of structure models present in
* a particular structure model group.
*/
ihm_model_group_link: {
/**
@@ -3587,7 +3727,7 @@ export const mmCIF_Schema = {
},
/**
* Data items in the IHM_MODEL_REPRESENTATIVE category record the
* details of the representative model in an ensemble or cluster.
* details of the representative structure model in an ensemble or cluster.
*/
ihm_model_representative: {
/**
@@ -3628,7 +3768,7 @@ export const mmCIF_Schema = {
/**
* The type of data held in the dataset.
*/
data_type: Aliased<'NMR data' | '3DEM volume' | '2DEM class average' | 'EM raw micrographs' | 'X-ray diffraction data' | 'SAS data' | 'CX-MS data' | 'Mass Spectrometry data' | 'EPR data' | 'H/D exchange data' | 'Single molecule FRET data' | 'Ensemble FRET data' | 'Experimental model' | 'Comparative model' | 'Integrative model' | 'De Novo model' | 'Predicted contacts' | 'Mutagenesis data' | 'DNA footprinting data' | 'Hydroxyl radical footprinting data' | 'Yeast two-hybrid screening data' | 'Quantitative measurements of genetic interactions' | 'Other'>(str),
data_type: Aliased<'NMR data' | '3DEM volume' | '2DEM class average' | 'EM raw micrographs' | 'X-ray diffraction data' | 'SAS data' | 'CX-MS data' | 'Crosslinking-MS data' | 'Mass Spectrometry data' | 'EPR data' | 'H/D exchange data' | 'Single molecule FRET data' | 'Ensemble FRET data' | 'Experimental model' | 'Comparative model' | 'Integrative model' | 'De Novo model' | 'Predicted contacts' | 'Mutagenesis data' | 'DNA footprinting data' | 'Hydroxyl radical footprinting data' | 'Yeast two-hybrid screening data' | 'Quantitative measurements of genetic interactions' | 'Other'>(str),
/**
* A flag that indicates whether the dataset is archived in
* an IHM related database or elsewhere.
@@ -3650,7 +3790,7 @@ export const mmCIF_Schema = {
/**
* The application / utilization of the dataset group in modeling.
*/
application: Aliased<'restraint' | 'validation' | 'filter' | 'representation' | 'sampling' | 'other'>(str),
application: Aliased<'restraint' | 'validation' | 'filter' | 'representation' | 'sampling' | 'modeling' | 'other'>(str),
/**
* Additional details regarding the dataset group.
*/
@@ -3710,7 +3850,7 @@ export const mmCIF_Schema = {
/**
* The name of the database containing the dataset entry.
*/
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'Other'>(str),
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'ProteomeXchange' | 'Other'>(str),
/**
* The accession code for the database entry.
*/
@@ -4037,7 +4177,7 @@ export const mmCIF_Schema = {
/**
* The type of crosslinker used.
*/
linker_type: Aliased<'EDC' | 'DSS' | 'EGS' | 'BS3' | 'BS2G' | 'DST' | 'sulfo-SDA' | 'sulfo-SMCC' | 'DSSO' | 'DSG' | 'BSP' | 'BMSO' | 'DHSO' | 'CYS' | 'SDA' | 'DSA' | 'BrdU' | 'LCSDA' | 'CDI' | 'ADH' | 'L-Photo-Leucine' | 'KArGO' | 'BrEtY' | 'DSBU' | 'DSPP' | 'TBDSPP' | 'Other'>(str),
linker_type: Aliased<'EDC' | 'DSS' | 'EGS' | 'BS3' | 'BS2G' | 'DST' | 'sulfo-SDA' | 'sulfo-SMCC' | 'DSSO' | 'DSG' | 'BSP' | 'BMSO' | 'DHSO' | 'CYS' | 'SDA' | 'DSA' | 'BrdU' | 'LCSDA' | 'CDI' | 'ADH' | 'L-Photo-Leucine' | 'KArGO' | 'BrEtY' | 'DSBU' | 'DSPP' | 'TBDSPP' | 'DMTMM' | 'PDH' | 'Other'>(str),
/**
* Identifier to the crosslinking dataset.
* This data item is a pointer to the _ihm_dataset_list.id in the

View File

@@ -80,7 +80,8 @@ function computeN(state: RmsdTransformState) {
let sizeSq = 0.0;
for (let i = 0, _l = state.a.x.length; i < _l; i++) {
const L = Math.min(state.a.x.length, state.b.x.length);
for (let i = 0; i < L; i++) {
const aX = xsA[i] - cA[0], aY = ysA[i] - cA[1], aZ = zsA[i] - cA[2];
const bX = xsB[i] - cB[0], bY = ysB[i] - cB[1], bZ = zsB[i] - cB[2];

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Fred Ludlow <Fred.Ludlow@astx.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import { degToRad } from '../../../mol-math/misc';
@@ -79,8 +80,9 @@ const tmpPosX = Vec3();
/**
* Calculate the angles x-a1-a2 for all x where x is a heavy atom (not H) bonded to ap1.
*/
export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex): number[] {
export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex, ignoreHydrogens = true): [number[], number[]] {
const angles: number[] = [];
const anglesH: number[] = [];
unitA.conformation.position(unitA.elements[indexA], tmpPosA);
unitB.conformation.position(unitB.elements[indexB], tmpPosB);
Vec3.sub(tmpDir1, tmpPosB, tmpPosA);
@@ -90,9 +92,13 @@ export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: Str
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
Vec3.sub(tmpDir2, tmpPosX, tmpPosA);
angles.push(Vec3.angle(tmpDir1, tmpDir2));
} else if (!ignoreHydrogens) {
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
Vec3.sub(tmpDir2, tmpPosX, tmpPosA);
anglesH.push(Vec3.angle(tmpDir1, tmpDir2));
}
});
return angles;
return [angles, anglesH];
}
/**
@@ -137,4 +143,23 @@ export function calcPlaneAngle(structure: Structure, unitA: Unit.Atomic, indexA:
Vec3.cross(tmpDir2, neighbours[0], neighbours[1]);
return Math.abs((Math.PI / 2) - Vec3.angle(tmpDir2, tmpDir1));
}
export function closestHydrogenIndex(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
let hIndex = indexA;
unitA.conformation.position(unitA.elements[indexA], tmpPosA);
unitB.conformation.position(unitB.elements[indexB], tmpPosB);
Vec3.sub(tmpDir1, tmpPosB, tmpPosA);
let minDistSq = Vec3.squaredDistance(tmpPosA, tmpPosB);
eachBondedAtom(structure, unitA, indexA, (unitX: Unit.Atomic, indexX: StructureElement.UnitIndex) => {
if (typeSymbol(unitX, indexX) === Elements.H) {
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
const dist = Vec3.squaredDistance(tmpPosX, tmpPosB);
if (dist < minDistSq) {
minDistSq = dist;
hIndex = indexX;
}
}
});
return hIndex;
}

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Fred Ludlow <Fred.Ludlow@astx.com>
* @author Paul Pillot <paul.pillot@tandemai.com>
*
* based in part on NGL (https://github.com/arose/ngl)
*/
@@ -97,12 +98,12 @@ function testHalogenBond(structure: Structure, infoA: Features.Info, infoB: Feat
const donIndex = don.members[don.offsets[don.feature]];
const accIndex = acc.members[acc.offsets[acc.feature]];
const halogenAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
const [halogenAngles] = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
// Singly bonded halogen only (not bromide ion for example)
if (halogenAngles.length !== 1) return;
if (OptimalHalogenAngle - halogenAngles[0] > opts.angleMax) return;
const acceptorAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
const [acceptorAngles] = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
// Angle must be defined. Excludes water as acceptor. Debatable
if (acceptorAngles.length === 0) return;
if (acceptorAngles.some(acceptorAngle => OptimalAcceptorAngle - acceptorAngle > opts.angleMax)) return;

View File

@@ -1,15 +1,16 @@
/**
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Fred Ludlow <Fred.Ludlow@astx.com>
* @author Paul Pillot <paul.pillot@tandemai.com>
*
* based in part on NGL (https://github.com/arose/ngl)
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle } from '../chemistry/geometry';
import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle, closestHydrogenIndex } from '../chemistry/geometry';
import { FeaturesBuilder, Features } from './features';
import { typeSymbol, bondToElementCount, bondCount, formalCharge, compId, atomId } from '../chemistry/util';
import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
@@ -23,6 +24,7 @@ const GeometryParams = {
distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
backbone: PD.Boolean(true, { description: 'Include backbone-to-backbone hydrogen bonds' }),
accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
ignoreHydrogens: PD.Boolean(false, { description: 'Ignore explicit hydrogens in geometric constraints' }),
donAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
@@ -208,6 +210,7 @@ function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
function getGeometryOptions(props: GeometryProps) {
return {
ignoreHydrogens: props.ignoreHydrogens,
includeBackbone: props.backbone,
maxAccAngleDev: degToRad(props.accAngleDevMax),
maxDonAngleDev: degToRad(props.donAngleDevMax),
@@ -230,26 +233,32 @@ type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
const deg120InRad = degToRad(120);
function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
const donIndex = don.members[don.offsets[don.feature]];
const accIndex = acc.members[acc.offsets[acc.feature]];
if (!opts.includeBackbone && isBackboneHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return;
const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
const [donAngles, donHAngles] = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex, opts.ignoreHydrogens);
const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || deg120InRad;
if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return;
if (donHAngles.length && !donHAngles.some(donHAngles => donHAngles < opts.maxDonAngleDev)) return;
if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex);
if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return;
}
const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
let donorIndex = donIndex;
if (!opts.ignoreHydrogens && donHAngles.length > 0) {
donorIndex = closestHydrogenIndex(structure, don.unit, donIndex, acc.unit, accIndex);
}
const [accAngles, accHAngles] = calcAngles(structure, acc.unit, accIndex, don.unit, donorIndex, opts.ignoreHydrogens);
const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || deg120InRad;
// Do not limit large acceptor angles
if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return;
if (accHAngles.some(accHAngles => idealAccAngle - accHAngles > opts.maxAccAngleDev)) return;
if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex);

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -19,7 +20,7 @@ import { Interval, OrderedSet, SortedArray } from '../../../mol-data/int';
import { Interactions } from '../interactions/interactions';
import { InteractionsProvider } from '../interactions';
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { InteractionFlag, InteractionType } from '../interactions/common';
import { FeatureType, InteractionFlag, InteractionType } from '../interactions/common';
import { Unit } from '../../../mol-model/structure/structure';
import { Sphere3D } from '../../../mol-math/geometry';
import { assertUnreachable } from '../../../mol-util/type-helpers';
@@ -54,7 +55,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
const uB = structure.unitMap.get(unitB) as Unit.Atomic;
if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && (
t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond)
t === InteractionType.HydrogenBond || (t === InteractionType.WeakHydrogenBond && ignoreHydrogensVariant !== 'non-polar'))
) {
const idxA = fA.members[fA.offsets[indexA]];
const idxB = fB.members[fB.offsets[indexB]];
@@ -64,30 +65,34 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
let minDistB = minDistA;
Vec3.copy(posA, pA);
Vec3.copy(posB, pB);
const donorType = t === InteractionType.HydrogenBond ? FeatureType.HydrogenDonor : FeatureType.WeakHydrogenDonor;
const isHydrogenDonorA = fA.types[fA.offsets[indexA]] === donorType;
eachBondedAtom(structure, uA, idxA, (u, idx) => {
const eI = u.elements[idx];
if (isHydrogen(structure, u, eI, 'polar')) {
u.conformation.position(eI, p);
const dist = Vec3.distance(p, pB);
if (dist < minDistA) {
minDistA = dist;
Vec3.copy(posA, p);
if (isHydrogenDonorA) {
eachBondedAtom(structure, uA, idxA, (u, idx) => {
const eI = u.elements[idx];
if (isHydrogen(structure, u, eI, 'all')) {
u.conformation.position(eI, p);
const dist = Vec3.distance(p, pB);
if (dist < minDistA) {
minDistA = dist;
Vec3.copy(posA, p);
}
}
}
});
eachBondedAtom(structure, uB, idxB, (u, idx) => {
const eI = u.elements[idx];
if (isHydrogen(structure, u, eI, 'polar')) {
u.conformation.position(eI, p);
const dist = Vec3.distance(p, pA);
if (dist < minDistB) {
minDistB = dist;
Vec3.copy(posB, p);
});
} else {
eachBondedAtom(structure, uB, idxB, (u, idx) => {
const eI = u.elements[idx];
if (isHydrogen(structure, u, eI, 'all')) {
u.conformation.position(eI, p);
const dist = Vec3.distance(p, pA);
if (dist < minDistB) {
minDistB = dist;
Vec3.copy(posB, p);
}
}
}
});
});
}
} else {
Vec3.set(posA, fA.x[indexA], fA.y[indexA], fA.z[indexA]);
Vec3.transformMat4(posA, posA, uA.conformation.operator.matrix);

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Paul Pillot <paul.pillot@tandemai.com>
*/
import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
@@ -19,7 +20,7 @@ import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr
import { VisualUpdateState } from '../../../mol-repr/util';
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
import { Interactions } from '../interactions/interactions';
import { InteractionFlag } from '../interactions/common';
import { FeatureType, InteractionFlag } from '../interactions/common';
import { Sphere3D } from '../../../mol-math/geometry';
import { StructureGroup, isHydrogen } from '../../../mol-repr/structure/visual/util/common';
import { assertUnreachable } from '../../../mol-util/type-helpers';
@@ -40,7 +41,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
const features = interactions.unitsFeatures.get(unit.id);
const contacts = interactions.unitsContacts.get(unit.id);
const { x, y, z, members, offsets } = features;
const { x, y, z, members, offsets, types } = features;
const { edgeCount, a, b, edgeProps: { flag, type } } = contacts;
const { sizeFactor, ignoreHydrogens, ignoreHydrogensVariant, parentDisplay } = props;
@@ -56,7 +57,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
const t = type[edgeIndex];
if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && (
t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond)
t === InteractionType.HydrogenBond || (t === InteractionType.WeakHydrogenBond && ignoreHydrogensVariant !== 'non-polar'))
) {
const idxA = members[offsets[a[edgeIndex]]];
const idxB = members[offsets[b[edgeIndex]]];
@@ -66,28 +67,32 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
let minDistB = minDistA;
Vec3.copy(posA, pA);
Vec3.copy(posB, pB);
const donorType = t === InteractionType.HydrogenBond ? FeatureType.HydrogenDonor : FeatureType.WeakHydrogenDonor;
const isHydrogenDonorA = types[offsets[a[edgeIndex]]] === donorType;
eachIntraBondedAtom(unit, idxA, (_, idx) => {
if (isHydrogen(structure, unit, elements[idx], 'polar')) {
c.invariantPosition(elements[idx], p);
const dist = Vec3.distance(p, pB);
if (dist < minDistA) {
minDistA = dist;
Vec3.copy(posA, p);
if (isHydrogenDonorA) {
eachIntraBondedAtom(unit, idxA, (_, idx) => {
if (isHydrogen(structure, unit, elements[idx], 'all')) {
c.invariantPosition(elements[idx], p);
const dist = Vec3.distance(p, pB);
if (dist < minDistA) {
minDistA = dist;
Vec3.copy(posA, p);
}
}
}
});
eachIntraBondedAtom(unit, idxB, (_, idx) => {
if (isHydrogen(structure, unit, elements[idx], 'polar')) {
c.invariantPosition(elements[idx], p);
const dist = Vec3.distance(p, pA);
if (dist < minDistB) {
minDistB = dist;
Vec3.copy(posB, p);
});
} else {
eachIntraBondedAtom(unit, idxB, (_, idx) => {
if (isHydrogen(structure, unit, elements[idx], 'all')) {
c.invariantPosition(elements[idx], p);
const dist = Vec3.distance(p, pA);
if (dist < minDistB) {
minDistB = dist;
Vec3.copy(posB, p);
}
}
}
});
});
}
} else {
Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]);
Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]);

File diff suppressed because one or more lines are too long

View File

@@ -139,7 +139,7 @@ function atomGroupsSegmented({ unitTest, entityTest, chainTest, residueTest, ato
}
}
} else {
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
const { chainElementSegments } = unit.kind === Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
while (chainsIt.hasNext) {
@@ -213,7 +213,7 @@ function atomGroupsGrouped({ unitTest, entityTest, chainTest, residueTest, atomT
}
}
} else {
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
const { chainElementSegments } = unit.kind === Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
while (chainsIt.hasNext) {
const chainSegment = chainsIt.move();

View File

@@ -161,7 +161,7 @@ const entity = {
pdbx_mutation: p(l => l.unit.model.entities.data.pdbx_mutation.value(eK(l))),
pdbx_fragment: p(l => l.unit.model.entities.data.pdbx_fragment.value(eK(l))),
pdbx_ec: p(l => l.unit.model.entities.data.pdbx_ec.value(eK(l))),
pdbx_parent_entity_id: p(l => l.unit.model.entities.data.pdbx_parent_entity_id.value(eK(l))),
subtype: p(l => l.unit.model.entities.subtype.value(eK(l))),
prd_id: p(l => l.unit.model.entities.prd_id?.value(eK(l)) ?? ''),
};
@@ -197,4 +197,4 @@ const StructureProperties = {
};
type StructureProperties = typeof StructureProperties
export { StructureProperties };
export { StructureProperties };

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -31,7 +31,7 @@ const DownloadDensity = StateAction.build({
'pdb-xray': PD.Group({
provider: PD.Group({
id: PD.Text('1tqn', { label: 'Id' }),
server: PD.Select('rcsb', [['pdbe', 'PDBe'], ['rcsb', 'RCSB PDB']]),
server: PD.Select('pdbe', [['pdbe', 'PDBe']]),
}, { pivot: 'id' }),
type: PD.Select('2fofc', [['2fofc', '2Fo-Fc'], ['fofc', 'Fo-Fc']]),
}, { isFlat: true }),
@@ -74,18 +74,12 @@ const DownloadDensity = StateAction.build({
downloadParams = src.params;
break;
case 'pdb-xray':
downloadParams = src.params.provider.server === 'pdbe' ? {
downloadParams = {
url: Asset.Url(src.params.type === '2fofc'
? `https://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.provider.id.toLowerCase()}.ccp4`
: `https://www.ebi.ac.uk/pdbe/coordinates/files/${src.params.provider.id.toLowerCase()}_diff.ccp4`),
isBinary: true,
label: `PDBe X-ray map: ${src.params.provider.id}`
} : {
url: Asset.Url(src.params.type === '2fofc'
? `https://edmaps.rcsb.org/maps/${src.params.provider.id.toLowerCase()}_2fofc.dsn6`
: `https://edmaps.rcsb.org/maps/${src.params.provider.id.toLowerCase()}_fofc.dsn6`),
isBinary: true,
label: `RCSB X-ray map: ${src.params.provider.id}`
};
break;
case 'pdb-emd-ds':
@@ -123,9 +117,7 @@ const DownloadDensity = StateAction.build({
break;
case 'pdb-xray':
entryId = src.params.provider.id;
provider = src.params.provider.server === 'pdbe'
? plugin.dataFormats.get('ccp4')
: plugin.dataFormats.get('dsn6');
provider = plugin.dataFormats.get('ccp4');
break;
case 'pdb-emd-ds':
case 'pdb-xray-ds':

View File

@@ -11,7 +11,13 @@ import { PluginStateAnimation } from '../model';
async function setPartialSnapshot(plugin: PluginContext, entry: PluginStateSnapshotManager.Entry, first = false) {
if (entry.snapshot.data) {
await plugin.runTask(plugin.state.data.setSnapshot(entry.snapshot.data));
// update the canvas3d trackball with the snapshot
plugin.canvas3d?.setProps({
trackball: entry.snapshot.canvas3d?.props?.trackball
});
}
if (entry.snapshot.camera) {
plugin.canvas3d?.requestCameraReset({
snapshot: entry.snapshot.camera.current,
@@ -77,7 +83,7 @@ export const AnimateStateSnapshots = PluginStateAnimation.create({
return { kind: 'skip' };
}
setPartialSnapshot(ctx.plugin, animState.snapshots[i]);
await setPartialSnapshot(ctx.plugin, animState.snapshots[i]);
return { kind: 'next', state: { ...animState, currentIndex: i } };
}

View File

@@ -1,8 +1,9 @@
/**
* Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Neli Fonseca <neli@ebi.ac.uk>
*/
import * as CCP4 from '../../mol-io/reader/ccp4/parser';
@@ -23,9 +24,12 @@ import { ColorNames } from '../../mol-util/color/names';
import { assertUnreachable } from '../../mol-util/type-helpers';
import { parsePrmtop } from '../../mol-io/reader/prmtop/parser';
import { parseTop } from '../../mol-io/reader/top/parser';
import { ungzip } from '../../mol-util/zip/zip';
import { utf8Read } from '../../mol-io/common/utf8';
export { Download };
export { DownloadBlob };
export { DeflateData };
export { RawData };
export { ReadFile };
export { ParseBlob };
@@ -134,6 +138,33 @@ const DownloadBlob = PluginStateTransform.BuiltIn({
// }
});
type DeflateData = typeof DeflateData
const DeflateData = PluginStateTransform.BuiltIn({
name: 'defalate-data',
display: { name: 'Deflate', description: 'Deflate compressed data' },
params: {
method: PD.Select('gzip', [['gzip', 'gzip']]), // later on we might have to add say brotli
isString: PD.Boolean(false),
stringEncoding: PD.Optional(PD.Select('utf-8', [['utf-8', 'UTF8']])),
label: PD.Optional(PD.Text(''))
},
from: [SO.Data.Binary],
to: [SO.Data.Binary, SO.Data.String]
})({
apply({ a, params }, plugin: PluginContext) {
return Task.create('Gzip', async ctx => {
const decompressedData = await ungzip(ctx, a.data);
const label = params.label ? params.label : a.label;
// handle decoding based on stringEncoding param
if (params.isString) {
const textData = utf8Read(decompressedData, 0, decompressedData.length);
return new SO.Data.String(textData, { label });
}
return new SO.Data.Binary(decompressedData as Uint8Array, { label });
});
}
});
type RawData = typeof RawData
const RawData = PluginStateTransform.BuiltIn({
name: 'raw-data',

View File

@@ -95,13 +95,13 @@ export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends Plugi
render() {
if (this.state.isHidden) return null;
const divid = this.state.header.toLowerCase().replace(/\s/g, '');
const wrapClass = this.state.isCollapsed
? 'msp-transform-wrapper msp-transform-wrapper-collapsed'
: 'msp-transform-wrapper';
return <div className={wrapClass}>
<div className='msp-transform-header'>
<div id={divid} className='msp-transform-header'>
<Button icon={this.state.brand ? void 0 : this.state.isCollapsed ? ArrowRightSvg : ArrowDropDownSvg} noOverflow onClick={this.toggleCollapsed}
className={this.state.brand ? `msp-transform-header-brand msp-transform-header-brand-${this.state.brand.accent}` : void 0} title={`Click to ${this.state.isCollapsed ? 'expand' : 'collapse'}`}>
{/* {this.state.brand && <div className={`msp-accent-bg-${this.state.brand.accent}`}>{this.state.brand.svg ? <Icon svg={this.state.brand.svg} /> : this.state.brand.name}</div>} */}
@@ -122,4 +122,4 @@ export abstract class CollapsableControls<P = {}, S = {}, SS = {}> extends Plugi
if (props.header !== undefined) state.header = props.header;
this.state = state;
}
}
}

View File

@@ -207,7 +207,7 @@ export function ViewportSnapshotDescription() {
const e = plugin.managers.snapshot.getEntry(current)!;
if (!e?.description?.trim()) return null;
return <div className='msp-snapshot-description-wrapper'>
return <div id='snapinfo' className='msp-snapshot-description-wrapper'>
<Markdown skipHtml components={{ a: MarkdownAnchor }}>{e.description}</Markdown>
</div>;
}

View File

@@ -169,9 +169,19 @@ const _SubscriptionsOutlined = <svg width='24px' height='24px' viewBox='0 0 24 2
export function SubscriptionsOutlinedSvg() { return _SubscriptionsOutlined; }
const _SwapHoriz = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M6.99 11L3 15l3.99 4v-3H14v-2H6.99v-3zM21 9l-3.99-4v3H10v2h7.01v3L21 9z' /></svg>;
export function SwapHorizSvg() { return _SwapHoriz; }
const _Tour = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M21 4H7V2H5v20h2v-8h14l-2-5zm-6 5c0 1.1-.9 2-2 2s-2-.9-2-2 .9-2 2-2 2 .9 2 2' /></svg>;
export function TourSvg() { return _Tour; }
const _Tune = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M3 17v2h6v-2H3zM3 5v2h10V5H3zm10 16v-2h8v-2h-8v-2h-2v6h2zM7 9v2H3v2h4v2h2V9H7zm14 4v-2H11v2h10zm-6-4h2V7h4V5h-4V3h-2v6z' /></svg>;
export function TuneSvg() { return _Tune; }
const _VisibilityOffOutlined = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M12 6c3.79 0 7.17 2.13 8.82 5.5-.59 1.22-1.42 2.27-2.41 3.12l1.41 1.41c1.39-1.23 2.49-2.77 3.18-4.53C21.27 7.11 17 4 12 4c-1.27 0-2.49.2-3.64.57l1.65 1.65C10.66 6.09 11.32 6 12 6zm-1.07 1.14L13 9.21c.57.25 1.03.71 1.28 1.28l2.07 2.07c.08-.34.14-.7.14-1.07C16.5 9.01 14.48 7 12 7c-.37 0-.72.05-1.07.14zM2.01 3.87l2.68 2.68C3.06 7.83 1.77 9.53 1 11.5 2.73 15.89 7 19 12 19c1.52 0 2.98-.29 4.32-.82l3.42 3.42 1.41-1.41L3.42 2.45 2.01 3.87zm7.5 7.5l2.61 2.61c-.04.01-.08.02-.12.02-1.38 0-2.5-1.12-2.5-2.5 0-.05.01-.08.01-.13zm-3.4-3.4l1.75 1.75c-.23.55-.36 1.15-.36 1.78 0 2.48 2.02 4.5 4.5 4.5.63 0 1.23-.13 1.77-.36l.98.98c-.88.24-1.8.38-2.75.38-3.79 0-7.17-2.13-8.82-5.5.7-1.43 1.72-2.61 2.93-3.53z' /></svg>;
const _TooltipText = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M20 2H4c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2M6 9h12v2H6zm8 5H6v-2h8zm4-6H6V6h12z' /></svg>;
export function TooltipTextSvg() { return _TooltipText; }
const _TooltipTextOutline = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M4 4h16v12H5.17L4 17.17zm0-2c-1.1 0-1.99.9-1.99 2L2 22l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm2 10h8v2H6zm0-3h12v2H6zm0-3h12v2H6z' /></svg>;
export function TooltipTextOutlineSvg() { return _TooltipTextOutline; }
const _PlusBox = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m-2 10h-4v4h-2v-4H7v-2h4V7h2v4h4z' /></svg>;
export function PlusBoxSvg() { return _PlusBox; }
const _MinusBox = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2m-2 10H7v-2h10z' /></svg>;
export function MinusBoxSvg() { return _MinusBox; }
export function VisibilityOffOutlinedSvg() { return _VisibilityOffOutlined; }
const _VisibilityOutlined = <svg width='24px' height='24px' viewBox='0 0 24 24'><path d='M12 6c3.79 0 7.17 2.13 8.82 5.5C19.17 14.87 15.79 17 12 17s-7.17-2.13-8.82-5.5C4.83 8.13 8.21 6 12 6m0-2C7 4 2.73 7.11 1 11.5 2.73 15.89 7 19 12 19s9.27-3.11 11-7.5C21.27 7.11 17 4 12 4zm0 5c1.38 0 2.5 1.12 2.5 2.5S13.38 14 12 14s-2.5-1.12-2.5-2.5S10.62 9 12 9m0-2c-2.48 0-4.5 2.02-4.5 4.5S9.52 16 12 16s4.5-2.02 4.5-4.5S14.48 7 12 7z' /></svg>;
export function VisibilityOutlinedSvg() { return _VisibilityOutlined; }

View File

@@ -83,7 +83,7 @@ class Layout extends PluginUIComponent {
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
}
region(kind: RegionKind, Element?: React.ComponentClass) {
region(kind: RegionKind, Element?: React.ComponentClass | React.FC) {
return <div className={`msp-layout-region msp-layout-${kind}`}>
<div className='msp-layout-static'>
{Element ? <Element /> : null}
@@ -268,6 +268,7 @@ export class ControlsWrapper extends PluginUIComponent {
export class DefaultViewport extends PluginUIComponent {
render() {
const VPControls = this.plugin.spec.components?.viewport?.controls || ViewportControls;
const SnapshotDescription = this.plugin.spec.components?.viewport?.snapshotDescription || ViewportSnapshotDescription;
return <>
<Viewport />
@@ -275,7 +276,7 @@ export class DefaultViewport extends PluginUIComponent {
<AnimationViewportControls />
<TrajectoryViewportControls />
<StateSnapshotViewportControls />
<ViewportSnapshotDescription />
<SnapshotDescription />
</div>
<SelectionViewportControls />
<VPControls />

View File

@@ -34,5 +34,7 @@
margin: $control-spacing;
}
background: $default-background;
& {
background: $default-background;
}
}

View File

@@ -345,6 +345,12 @@
text-align: left;
color: color-lower-contrast($font-color, 15%);
}
> p {
padding: (math.div($control-spacing, 2)) $control-spacing;
text-align: left;
color: color-lower-contrast($font-color, 15%);
}
}
.msp-help-description {

View File

@@ -28,7 +28,9 @@
margin: 0;
}
color: $log-font-color;
& {
color: $log-font-color;
}
li {
clear: both;

View File

@@ -40,11 +40,14 @@
b {
font-size: 120%;
}
display: table-cell;
vertical-align: middle;
text-align: center;
width: 100%;
height: 100%;
& {
display: table-cell;
vertical-align: middle;
text-align: center;
width: 100%;
height: 100%;
}
}
}
@@ -356,7 +359,9 @@
font-weight: bold;
}
border-radius: 0 !important;
& {
border-radius: 0 !important;
}
}
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -19,13 +19,14 @@ interface PluginUISpec extends PluginSpec {
components?: {
controls?: PluginUISpec.LayoutControls
remoteState?: 'none' | 'default',
structureTools?: React.ComponentClass,
structureTools?: React.ComponentClass | React.FC,
viewport?: {
view?: React.ComponentClass,
controls?: React.ComponentClass
view?: React.ComponentClass | React.FC,
controls?: React.ComponentClass | React.FC,
snapshotDescription?: React.ComponentClass | React.FC,
},
sequenceViewer?: {
view?: React.ComponentClass
view?: React.ComponentClass | React.FC
}
hideTaskOverlay?: boolean,
disableDragOverlay?: boolean,
@@ -34,10 +35,10 @@ interface PluginUISpec extends PluginSpec {
namespace PluginUISpec {
export interface LayoutControls {
top?: React.ComponentClass | 'none',
left?: React.ComponentClass | 'none',
right?: React.ComponentClass | 'none',
bottom?: React.ComponentClass | 'none'
top?: React.ComponentClass | React.FC | 'none',
left?: React.ComponentClass | React.FC | 'none',
right?: React.ComponentClass | React.FC | 'none',
bottom?: React.ComponentClass | React.FC | 'none'
}
}

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2022-2024 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 { PresetStructureRepresentations } from '../../mol-plugin-state/builder/structure/representation-preset';
@@ -70,6 +71,7 @@ export class QuickStyles extends PurePluginUIComponent {
radius: 5,
bias: 0.8,
blurKernelSize: 15,
blurDepthBias: 0.5,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
@@ -108,6 +110,7 @@ export class QuickStyles extends PurePluginUIComponent {
radius: 5,
bias: 0.8,
blurKernelSize: 15,
blurDepthBias: 0.5,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
@@ -121,15 +124,15 @@ export class QuickStyles extends PurePluginUIComponent {
render() {
return <div className='msp-flex-row'>
<Button noOverflow title='Applies default representation preset. Set outline and occlusion effects to defaults.' onClick={() => this.default()} style={{ width: 'auto' }}>
<Button noOverflow title='Applies default representation preset and sets outline and occlusion effects to default' onClick={() => this.default()} style={{ width: 'auto' }}>
Default
</Button>
<Button noOverflow title='Applies no representation preset. Enables outline and occlusion effects. Enables ignore-light representation parameter.' onClick={() => this.stylized()} style={{ width: 'auto' }}>
Stylized
</Button>
<Button noOverflow title='Applies illustrative representation preset. Enables outline and occlusion effects. Enables ignore-light parameter.' onClick={() => this.illustrative()} style={{ width: 'auto' }}>
<Button noOverflow title='Applies illustrative representation preset and Stylize it' onClick={() => this.illustrative()} style={{ width: 'auto' }}>
Illustrative
</Button>
<Button noOverflow title='Does not change representation, enables outline and occlusion effects, enables ignore-light representation parameter' onClick={() => this.stylized()} style={{ width: 'auto' }}>
Stylize Current
</Button>
</div>;
}
}

View File

@@ -63,6 +63,7 @@ const SimpleSettingsParams = {
occlusion: Canvas3DParams.postprocessing.params.occlusion,
shadow: Canvas3DParams.postprocessing.params.shadow,
outline: Canvas3DParams.postprocessing.params.outline,
dof: Canvas3DParams.postprocessing.params.dof,
fog: Canvas3DParams.cameraFog,
}, { isFlat: true }),
clipping: PD.Group<any>({
@@ -129,6 +130,7 @@ const SimpleSettingsMapping = ParamMapping({
occlusion: canvas.postprocessing.occlusion,
shadow: canvas.postprocessing.shadow,
outline: canvas.postprocessing.outline,
dof: canvas.postprocessing.dof,
fog: canvas.cameraFog,
},
clipping: {
@@ -162,6 +164,7 @@ const SimpleSettingsMapping = ParamMapping({
canvas.multiSample = s.advanced.multiSample;
canvas.hiZ = s.advanced.hiZ;
canvas.postprocessing.sharpening = s.advanced.sharpening;
canvas.postprocessing.dof = s.lighting.dof;
props.layout = s.layout;
props.pixelScale = s.advanced.pixelScale;

View File

@@ -19,7 +19,7 @@ export class PluginAnimationLoop {
return this._isAnimating;
}
async tick(t: number, options?: { isSynchronous?: boolean, manualDraw?: boolean, animation?: PluginAnimationManager.AnimationInfo }) {
async tick(t: number, options?: { isSynchronous?: boolean, manualDraw?: boolean, animation?: PluginAnimationManager.AnimationInfo, updateControls?: boolean}) {
await this.plugin.managers.animation.tick(t, options?.isSynchronous, options?.animation);
this.plugin.canvas3d?.tick(t as now.Timestamp, options);

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2024 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>
@@ -374,7 +374,7 @@ export class PluginContext {
return PluginCommands.State.RemoveObject(this, { state: this.state.data, ref: StateTransform.RootRef });
}
dispose(options?: { doNotForceWebGLContextLoss?: boolean }) {
dispose(options?: { doNotForceWebGLContextLoss?: boolean, doNotDisposeCanvas3DContext?: boolean }) {
if (this.disposed) return;
for (const s of this.subs) {
@@ -385,7 +385,9 @@ export class PluginContext {
this.animationLoop.stop();
this.commands.dispose();
this.canvas3d?.dispose();
this.canvas3dContext?.dispose(options);
if (!options?.doNotDisposeCanvas3DContext) {
this.canvas3dContext?.dispose(options);
}
this.ev.dispose();
this.state.dispose();
this.helpers.substructureParent.dispose();

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