Compare commits

...

208 Commits

Author SHA1 Message Date
dsehnal
1bd162b977 tweaks 2025-01-04 11:07:55 +01:00
dsehnal
c7fb71738e header 2025-01-04 11:03:01 +01:00
dsehnal
9413481253 Fix plugin interactions when CSS scale is applied 2025-01-04 11:02:37 +01:00
Alexander Rose
9f3c617945 Merge pull request #1397 from molstar/color-external-structure
add external-structure theme
2025-01-02 14:45:50 -08:00
Alexander Rose
f920188cdc Merge pull request #1398 from ventura-rivera/master
fix hyperlink typo
2025-01-02 14:45:33 -08:00
Ventura Rivera
68b73503bb update changelog with doc updates 2025-01-02 12:59:35 -08:00
Ventura Rivera
e776138ecd fix hyperlink typo 2025-01-02 12:55:42 -08:00
Alexander Rose
bbacd5a9dd Merge branch 'master' into color-external-structure 2024-12-31 13:53:04 -08:00
Alexander Rose
289ecef1d7 add approxNearest to lookup3d 2024-12-31 13:31:26 -08:00
Alexander Rose
52fc3ef750 Merge pull request #1396 from molstar/float-volume-data
Support float and half-float data type
2024-12-29 18:41:53 -08:00
Alexander Rose
dffe40ac1d simplify isosurface property handling 2024-12-29 17:54:23 -08:00
Alexander Rose
f834e39ce4 add external-structure theme
- colors any geometry by structure properties
2024-12-28 17:38:49 -08:00
Alexander Rose
2bc9c6fb57 Support float and half-float data type
- direct-volume rendering
- GPU isosurface extraction
2024-12-28 14:29:17 -08:00
Alexander Rose
6e42c11f5e comments regarding webgl1 consistent bit plane counts 2024-12-28 13:08:05 -08:00
midlik
d48feeaa94 Mvs union params (#1388)
* MVS: define union params

* MVS: include defaults in param schema

* MVS: removed mvs-defaults

* MVS: union params validation

* MVS: use new param schema

* MVS: Nicely format ColorName

* MVS: primitive uses UnionParamsSchema

* MVS: mesh remove triangle_groups

* MVS: remove dead code

* MVS: reorg files and add docs for parameter system

* MVS: print-schema for union params

* MVS: remove line_colors

* MVS: Rename many primitives params

* MVS: update primitive params descriptions

* MVS: Refactor primitive Builders

* Volumes and segmentations: avoid parsing non-success response

* MVS: refactor primitive params types

* MVS: avoid repeating MVS defaults in primitives.ts

* MVS: update builder

* MVS: primitive params docstrings
2024-12-18 18:54:39 +01:00
David Sehnal
fd0ca75fc1 Volume UI improvements (#1379)
* improvements to Volumes UI

* support wheel scroll on sliders

* headers

* changelog
2024-12-17 17:55:06 +01:00
Alexander Rose
a270dcb5f5 error handling in deploy script 2024-12-15 13:49:54 -08:00
Alexander Rose
917de1175c 4.10.0 2024-12-15 10:05:40 -08:00
Alexander Rose
65945fb904 changelog 2024-12-15 10:01:58 -08:00
Alexander Rose
3b7afc6037 package updates 2024-12-15 10:01:15 -08:00
Alexander Rose
05d9ca6e68 Merge pull request #1386 from molstar/fix-handle-resize
wip, fix handle resize
2024-12-15 09:52:09 -08:00
Alexander Rose
12ee0e0f38 Fix resize handling in tests/browser 2024-12-15 09:42:03 -08:00
Alexander Rose
dbd29e749e Merge branch 'master' of https://github.com/molstar/molstar into fix-handle-resize 2024-12-15 08:52:57 -08:00
Alexander Rose
a8085111dc add support for more webgl extensions
- EXT_render_snorm
- WEBGL_render_shared_exponent
- EXT_texture_norm16
- EXT_depth_clamp
2024-12-14 11:59:18 -08:00
Alexander Rose
93798554ac Use adjoint matrix to transform normals in shaders 2024-12-14 11:55:47 -08:00
Alexander Rose
ce07c52d9f Fix addIndexPairBonds quadratic runtime case 2024-12-14 11:53:07 -08:00
Alexander Rose
fb7a247f6c Fix units transform data not fully updated when structure child changes 2024-12-14 11:51:10 -08:00
Alexander Rose
e9dfe6322d changelog 2024-12-14 11:50:27 -08:00
Alexander Rose
079187326a wip, fix handle resize 2024-12-12 21:58:15 -08:00
midlik
4dc9d037a4 MolViewSpec animations (#1348)
* MVS: Interfaces for multistate specs (wip)

* MVS: Multistate loading PoC

* MVS: Multistate loading PoC with camera

* Focus node in MOLJ: dirty impl

* PluginCommands.Camera.FocusObject

* Minor changes

* Refactoring for FocusObject

* Refactor getFocusBoundingSphere

* Move stuff to src/mol-plugin-state/manager/focus-camera/focus-object.ts

* MVS: molstarTreeToEntry include focus

* MVS: multi-state with focus

* MVS: multi-state with implicit camera

* MVS: multi-state works with video rendering

* Fix is_iOS() for NodeJS

* MVS: multi-state with canvas node

* Added PluginStateSnapshotManager.EntryParams.descriptionFormat

* MVS: mvs-render can generate MP4

* MVS: Remove dead code

* Update CHANGELOG

* MVS: Rename linger_duration_ms, transition_duration_ms

* MVS: focus node, radius* params

* PluginState.Snapshot.camera.focus allow multiple targets

* MVS: support multiple focus nodes

* MVS: Rename param radius_extend -> radius_extent

* MVS: support focus node on root

* MVS: Synchonize metadata format with backend

* MVS: change "transparency" to "opacity"

* Updated CHANGELOG
2024-12-12 13:06:23 +01:00
Alexander Rose
f36ad9ac28 Merge pull request #1380 from molstar/fix-gap-marking-consecutive
fix marking of consecutive gap elements
2024-12-09 19:21:34 -08:00
Alexander Rose
6d392de628 Merge branch 'master' into fix-gap-marking-consecutive 2024-12-09 19:21:03 -08:00
Alexander Rose
d7cd957b42 fix missing deflate header if CompressionStream is available 2024-12-09 19:15:13 -08:00
Alexander Rose
de36612bf1 changelog 2024-12-09 19:13:45 -08:00
Yakov Pechersky
d5154bcff2 Support React 19 (#1382)
By loosening `peerDependencies`
2024-12-09 15:51:34 +01:00
Alexander Rose
b44a6fa660 fix marking of consecutive gap elements 2024-12-08 16:33:58 -08:00
David Sehnal
5cc28c9471 Add ModelWithCoordinates transform (#1378)
* Add ModelWithCoordinates transform

* PR feedback
2024-12-08 18:21:13 +01:00
Alexander Rose
b42a6d4636 Merge pull request #1373 from giagitom/illumination-outlines-fix
Fix outlines on transparent background using illuminartion mode
2024-12-07 18:10:49 -08:00
Alexander Rose
efd405f44b Merge branch 'master' into illumination-outlines-fix 2024-12-07 18:10:14 -08:00
Alexander Rose
4b3932e9e2 4.9.1 2024-12-05 08:55:57 -08:00
Alexander Rose
dcb8eca29a changelog 2024-12-05 08:53:54 -08:00
midlik
ac0177aef5 Fix iOS check (#1376) 2024-12-05 10:37:15 +01:00
giagitom
316013aafd Fix transparent depth artifacts using illumination mode 2024-12-03 16:05:57 +01:00
giagitom
040d83e8d4 Fix outlines on transparent background using illuminartion mode 2024-12-03 15:26:36 +01:00
Alexander Rose
b31ed50b3a 4.9.0 2024-12-01 13:38:17 -08:00
Alexander Rose
2a9c4db97f lint 2024-12-01 13:34:35 -08:00
Alexander Rose
fbeda779ac changelog 2024-12-01 13:32:37 -08:00
Alexander Rose
89e60cfde9 Merge pull request #1372 from molstar/immutablejs5
update to immutable-js 5
2024-12-01 13:27:26 -08:00
Dominik Tichy
0845f5fd75 Fix: missing partial charges (#1368)
* fix: color missing charges green

* fix: save missing charges as undefined

* chore: updated changelog

* fix: corrected check of missing values

* fix: lint

* fix: wrong logic

* fix: removed unused functions

* fix: unecessary assignment of undefined
2024-12-01 19:38:47 +01:00
dsehnal
918b67482f update to immutable-js 5 2024-12-01 15:41:20 +01:00
Alexander Rose
3ff3ea2912 schema updates 2024-11-30 15:37:39 -08:00
Alexander Rose
b2e1d069ba add missing bytes member to File_NodeJs 2024-11-30 15:37:32 -08:00
Alexander Rose
0a409c6fdf fix missing params assignment in shape repr 2024-11-30 15:37:02 -08:00
Alexander Rose
5ce552d2cc package updates 2024-11-30 15:36:30 -08:00
Alexander Rose
8bda510378 fix wrong repr params assignment 2024-11-28 21:09:41 -08:00
midlik
ad1923f57b Servers object storage (#1355)
* ModelServer: support for GS

* VolumeServer: simple support for GS

* fetch_GS abort

* file reorg

* ModelServer/VolumeServer: dynamic import @google-cloud/storage

* ModelServer/VolumeServer: update docs

* ModelServer/VolumeServer: handle HTTP and GS errors

* ModelServer/VolumeServer: update docs

* ModelServer/VolumeServer: update docs 2

* Minor tweaks
2024-11-28 12:06:02 +01:00
Sebastian Bittrich
ba38fe2474 Membrane Server to generate MolViewSpec data (#1338)
* membrane server wip

* cleanup

* desc

* cl
2024-11-25 19:12:40 +01:00
Alexander Rose
c53b651472 Merge pull request #1359 from molstar/snapshot-manager-fix-current
PluginStateSnapshotManager.syncCurrent fix
2024-11-23 10:29:37 -08:00
Alexander Rose
2eb4f77504 Merge branch 'master' into snapshot-manager-fix-current 2024-11-23 10:29:29 -08:00
Alexander Rose
c09f30a135 Merge pull request #1353 from giagitom/x-ray-fix
Xray shading fix for high XrayEdgeFalloff
2024-11-23 10:06:41 -08:00
Alexander Rose
c60c52f563 Merge pull request #1349 from papillot/infer-valence-from-protonated-ligand
Do not infer implicit Hs when explicit Hs are set #1257
2024-11-23 10:05:51 -08:00
dsehnal
7e67678dcd PluginStateSnapshotManager.syncCurrent fix 2024-11-23 08:55:48 +01:00
giagitom
4ee33c9dcd Use clamp 2024-11-20 18:51:19 +01:00
giagitom
8a0d5eb366 Xray shading fix for high XrayEdgeFalloff 2024-11-20 15:56:22 +01:00
Paul Pillot
e18a3b452a Do not infer implicitH when explicitH are set
Instead of detecting the protonation at the level of the atom only, the protonation of the unit is detected first. If protonation is known, then the assignment of implicit hydrogens is not needed.
This avoids cases where a nitrogen on a protonated ligand gets assigned a positive charge when it has no hydrogens, because the code was assuming that protonation was unknown in that case.
2024-11-18 17:24:28 -05:00
Alexander Rose
38a508fd87 Merge pull request #1328 from sbittrich/master
Membrane orientation: improve `isApplicable` check and error handling
2024-11-16 19:26:45 -08:00
Alexander Rose
0b1fd14e09 Merge pull request #1337 from giagitom/inprove-tubular-helices
Inprove tubular helices
2024-11-16 19:22:23 -08:00
Alexander Rose
b883ddd10e Fix transform data not updated when structure child changes 2024-11-16 12:47:30 -08:00
Simeon Borko
30557d13ca Refactor value swapping in molstar-math to fix SWC (Next.js) build (#1345) (#1346)
Co-authored-by: Simeon Borko <simeon.borko@recetox.muni.cz>
2024-11-16 08:08:28 +01:00
David Sehnal
85b72ae3b0 StructConn.isExhaustive fix (#1342) 2024-11-13 18:30:40 +01:00
JonStargaryen
2ed165f9a5 extend check to all models 2024-11-11 08:39:56 -08:00
giagitom
8c5388a6ea Cleanup 2024-11-11 17:25:39 +01:00
giagitom
703ef6c273 Enable double rounded cap on tubular helices 2024-11-11 17:16:17 +01:00
giagitom
0a1c5537d2 fix trace iterator for single residue tubular helices 2024-11-11 17:14:56 +01:00
Alexander Rose
e65f5b270e Merge pull request #1331 from giagitom/postprocessing-update-fix
Fix outlines on volume and surface reps that do not disappearing
2024-11-10 09:46:48 -08:00
Alexander Rose
9185c4592f Merge pull request #1329 from sbittrich/express
Update to express v5
2024-11-10 09:44:39 -08:00
Alexander Rose
fbe44bfab7 Merge branch 'master' into express 2024-11-10 09:44:07 -08:00
Alexander Rose
f4d44621d6 improve inter unit bond performance
- use numbers instead of strings in maps
- bespoke `eachStructureGroupsBond` to iterate
2024-11-10 09:35:44 -08:00
Alexander Rose
05a87fded9 Fix bonds not shown with ignoreHydrogens on (#1315)
- Better handle mmCIF files with no entities defined by using `label_asym_id`
- Show bonds in water chains when `ignoreHydorgensVariant` is `non-polar`
2024-11-10 09:33:25 -08:00
Alexander Rose
195f7284b5 fix transparent SSAO for image rendering 2024-11-10 09:27:43 -08:00
Alexander Rose
c4a900e2ea fix occupancy check using wrong index for inter-unit bond computation 2024-11-10 09:26:03 -08:00
giagitom
e1eb686355 Fix outlines on volume and surface reps that do not disappearing 2024-11-07 14:51:15 +01:00
Sebastian Bittrich
54b4a01cc3 update to express v5 (#1311) 2024-11-05 15:42:06 -08:00
Sebastian Bittrich
f68a01183d consider coarse-grained models 2024-11-05 10:24:08 -08:00
Sebastian Bittrich
057d605135 merge 2024-11-05 10:07:42 -08:00
Sebastian Bittrich
a391bbf786 cleanup 2024-11-05 10:04:54 -08:00
Sebastian Bittrich
fdc1054060 membrane orientation: improve isApplicable check and error handling (closes #1316) 2024-11-05 10:04:30 -08:00
Alexander Rose
b4238f574a fix incudeParent handling for structure-based visuals 2024-11-04 21:10:10 -08:00
Alexander Rose
965c6a37a9 Merge pull request #1325 from molstar/structure-visuals
Structure visuals
2024-11-03 21:27:16 -08:00
Alexander Rose
35a9056368 Add more structure-based visuals
-  to avoid too many (small) render-objects
- `structure-intra-bond`, `structure-ellipsoid-mesh`, `structure-element-point`, `structure-element-cross`
2024-11-02 17:39:04 -07:00
Alexander Rose
fd96973e82 Add Structure.intraUnitBondMapping 2024-11-02 17:38:22 -07:00
Alexander Rose
8812b0d264 Merge pull request #1320 from bergwerf/delete_fence_sync
Set fenceSync to null after deleteSync.
2024-11-02 11:13:55 -07:00
Alexander Rose
597c0dbbe1 Merge branch 'master' into delete_fence_sync 2024-11-02 11:11:24 -07:00
Alexander Rose
768d7a2a4d make ViewportScreenshotHelper.download return a promise 2024-11-02 11:10:29 -07:00
Alexander Rose
30ec53ffa4 Fix operator key-based IndexPairBonds assignment
- Don't add bonds twice
- Add `IndexPairs.bySameOperator` to avoid looping over all bonds for each unit
2024-11-02 11:09:16 -07:00
Dirk Arnez
b79ffd9cfc - fixed linting issues at glb-export example (#1322) 2024-11-01 14:11:41 +01:00
Herman Bergwerf
cc7f88fd53 Set fenceSync to null after deleteSync. 2024-10-31 11:27:53 +01:00
Dirk Arnez
57c84d0159 - add glb export example (#1314)
* - add glb export example

* Update src/examples/glb-export/index.ts

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-10-30 08:42:36 +01:00
Alexander Rose
4daf409337 4.8.0 2024-10-27 14:42:49 -07:00
Alexander Rose
a17e886ab9 changelog 2024-10-27 14:23:12 -07:00
Alexander Rose
ebb9046184 webpack: no fallback for vm package 2024-10-27 14:21:24 -07:00
Alexander Rose
cb41c0c7f9 package updates 2024-10-27 14:09:26 -07:00
Alexander Rose
bdbc9eab64 schema updates 2024-10-27 14:09:15 -07:00
Alexander Rose
5f76620ef5 Merge pull request #1308 from molstar/operator-bonds
Improve performance of `IndexPairBonds` assignment
2024-10-27 13:58:55 -07:00
Alexander Rose
b5c1c4d32e Merge branch 'master' into operator-bonds 2024-10-27 13:58:12 -07:00
Alexander Rose
6e4777355a Merge pull request #1309 from molstar/simplify-text-frag
remove extra anti-aliasing from text shader
2024-10-27 13:57:37 -07:00
David Sehnal
7526535a8b Basic support for Predicted Aligned Error parsing and plotting (#1302)
* proof of concept

* typos

* plot interactivity

* centerline

* better axis labels, darkmode support

* plot label

* data source selection

* interactivity

* better plot labels

* make pae overpaint ghosts

* config option

* update labels

* standalone mode and AFDB example

* fix interactivity

* pr feedback

* changelog
2024-10-27 07:36:22 +01:00
Alexander Rose
2bd84b7e7c remove extra anti-aliasing from text shader 2024-10-26 22:15:19 -07:00
Alexander Rose
84e292b3e2 Merge pull request #1293 from corredD/lammps_data
Lammps data
2024-10-26 17:34:30 -07:00
Alexander Rose
152cef9c5b tweaks & cleanup 2024-10-26 17:23:36 -07:00
Alexander Rose
d76bc583c0 Merge branch 'master' of https://github.com/molstar/molstar into pr/corredD/1293 2024-10-26 17:03:55 -07:00
Alexander Rose
071fb21dd0 Merge pull request #1252 from giagitom/transparent-ssao
Transparent ssao
2024-10-26 14:25:14 -07:00
Alexander Rose
db8943bcfb tweaks & cleanup 2024-10-26 14:16:41 -07:00
Alexander Rose
2d1ce14f2e fix marking not applied in illumination pass 2024-10-26 13:54:31 -07:00
Alexander Rose
33760b0d37 Improve performance of IndexPairBonds assignment when operator keys are available 2024-10-26 13:49:09 -07:00
giagitom
1aa6f30780 Optimize outlines 2024-10-23 13:01:04 +02:00
Ludovic Autin
86c8dd5d74 Merge commit 'c37a7ebf791afb8aa60ee8170d28a2bf156e7609' into lammps_data 2024-10-22 23:26:28 -07:00
Alexander Rose
1435a5e6e6 fix calling renderBlendedTransparent twice 2024-10-22 22:26:22 -07:00
Alexander Rose
c123e55a8d Merge branch 'master' of https://github.com/molstar/molstar into pr/giagitom/1252 2024-10-22 22:19:01 -07:00
Alexander Rose
c37a7ebf79 remove unused file 2024-10-22 22:17:25 -07:00
Alexander Rose
00e228a834 fix bloom in illumination mode 2024-10-22 22:17:16 -07:00
Ludovic Autin
55f40738f2 revert registry order. 2024-10-22 21:31:25 -07:00
Ludovic Autin
4ffd69750f Merge commit '12add4d66b93438eb53e6406811b8daea83bc907' into lammps_data 2024-10-22 21:30:03 -07:00
David Sehnal
295608baae fix Safari sequence view (#1305) 2024-10-22 20:09:15 +02:00
giagitom
4429b7185f assign material color fix 2024-10-22 19:30:30 +02:00
giagitom
84fadc2e5c Rename internal properties 2024-10-22 19:20:36 +02:00
giagitom
0b3bd885ca Fixes and removing includeTransparent property 2024-10-22 19:13:21 +02:00
giagitom
51d9eda168 Outlines fixes 2024-10-21 18:27:51 +02:00
giagitom
abe10d5c7c changed name and fixed behavior of transparentAlphaThreshold 2024-10-21 15:35:26 +02:00
giagitom
e7da2333fe Add transparentAlphaThreshold to disable transparent ssao on low transparency scenes (disabled on viewport screenshot) 2024-10-21 14:55:19 +02:00
Alexander Rose
3899a95c97 add scene.transparencyMin 2024-10-21 12:12:38 +02:00
Alexander Rose
12add4d66b me: add support for 4-character PDB ID in pdb-dev loader 2024-10-20 22:34:24 -07:00
Ludovic Autin
e16c073639 renaming 2024-10-19 14:32:04 -07:00
Ludovic Autin
3c5dc56bb2 follow @arose recommandation. 2024-10-19 14:18:52 -07:00
Alexander Rose
ad2106e6f6 use moleculeId as asym_id 2024-10-18 23:10:11 -07:00
Alexander Rose
dd5aa061b8 moleculeType -> moleculeId 2024-10-18 23:09:38 -07:00
Alexander Rose
f69ad14296 add scaling factor where it made sense 2024-10-18 23:08:48 -07:00
Alexander Rose
277254b78e fix atom style type error 2024-10-18 23:08:24 -07:00
ludovic autin
3c4f2806e7 David's recommandation 2024-10-18 16:44:51 -07:00
ludovic autin
79612833d4 Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-18 16:44:02 -07:00
midlik
b4772e0cb9 Fix binarySearchPredIndexRange (#1264)
* Fix binarySearchPredIndexRange

* Tests

* binarySearchPredIndexRange - avoid param reassignment

* Update CHANGELOG
2024-10-18 16:12:27 +02:00
David Sehnal
003c5a9437 MVS: Primitives MVP (#1300)
* inline primitives (mesh, line)

* tweak

* resolved positions

* wip labels

* tooltips

* refactor primitives

* primitives from URI

* primitives focus support todo

* focusable render objects

* default label color

* move code

* label primitive

* default shape provider params

* refactoring

* support primitive instancing

* tweak

* mvs refs support

* changelog

* header

* streamline primitives from uri

* group manager and pass node data to shape

* better position typing and resolution

* imports

* fix bug

* lint

* support mesh wireframe

* lines primitive
2024-10-18 15:00:57 +02:00
ludovic autin
ff9fb450fa added unitStyle information, reorganize the io parser. Fix spelling 2024-10-17 12:25:36 -07:00
ludovic autin
136e996e4f Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-17 12:24:11 -07:00
Alexander Rose
a93b53c413 fix for "cleanup illumination pass" 2024-10-16 22:28:27 -07:00
ludovic autin
0f25421db1 selection helper/shortcut to handle atom types ( coveres regular atom types C,N,O etc.. and custom atom type like in lammps eg . 1-200 etc...) 2024-10-16 15:37:19 -07:00
ludovic autin
cde3a73bba Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-16 15:34:10 -07:00
midlik
c19130c9eb MVS transparency and additional properties (#1289)
* MVS: Transparency support

* MVS: Data model supporting additional properties

* MVS: Builder supporting additional properties

* MVS: Loading extensions, NonCovalentInteractionsExtension

* MVS: Refactor, update CHANGELOG

* MVS: Rename additional_properties -> custom

* MVS: Add "ref" to nodes

* MVS: Fix missing dealing with ref

* MVS: Builder supporting custom and ref

* MVS: minor refactor

* StateBuilder support adding tags

* MVS: Add node ref as tag

---------

Co-authored-by: David Sehnal <dsehnal@users.noreply.github.com>
2024-10-16 18:17:25 +02:00
giagitom
54c8801951 - Disable transparent ssao if nothing transparent to render
- Fix transparent color texture not cleared when scene.opacityAverage = 1
2024-10-15 16:22:25 +02:00
giagitom
8371a3e349 Merge remote-tracking branch 'upstream/master' into transparent-ssao 2024-10-15 14:55:20 +02:00
Alexander Rose
cca289728c add sample/blur timer mark to ssao pass 2024-10-14 23:04:59 -07:00
Alexander Rose
d9a44daa5d cleanup illumination pass
- reuse buffers from draw pass
- remove superfluous anti aliasing render
2024-10-14 22:50:45 -07:00
Alexander Rose
48ee9ef8cb fix Scene.opacityAverage calculation never 1 2024-10-14 22:46:47 -07:00
ludovic autin
ba84081888 Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-14 11:21:14 -07:00
ludovic autin
45d8059ed2 removed scale options. Fix transformation when coordinates are scaled. 2024-10-14 11:20:28 -07:00
giagitom
6e2d8653ec Fix transparent outline size 2024-10-14 17:01:30 +02:00
Alexander Rose
cca6210076 rename includeTransparency -> includeTransparent
- to be same as in outline pass
2024-10-13 22:37:28 -07:00
Alexander Rose
9f926757b2 relax ssao blur background check 2024-10-13 22:32:00 -07:00
Alexander Rose
87d83d8f9e more ssao timers 2024-10-13 22:31:15 -07:00
Alexander Rose
d16076b170 cleanup 2024-10-13 22:30:38 -07:00
Alexander Rose
cccdc53fd0 merge master 2024-10-13 22:26:56 -07:00
Alexander Rose
a312799361 Merge pull request #1297 from molstar/fix-backfaces-visible
fix backfaces visible using blended transparency on impostors
2024-10-13 13:46:39 -07:00
Alexander Rose
60c81e79ba illumination ssao fixes & cleanup 2024-10-12 09:34:32 -07:00
Alexander Rose
bd22db4252 fix backfaces visible using blended transparency on impostors 2024-10-12 09:26:15 -07:00
Alexander Rose
36b5a9e181 only request draw after interaction in illumination mode 2024-10-12 09:08:00 -07:00
ludovic autin
809cca5261 lammps can dump normalize coordinates (sx or usx), to be able to handle it, parse the bounding box, and apply the transformation in that case 2024-10-11 14:53:43 -07:00
ludovic autin
7a81ea3ba1 missing arguments 2024-10-10 13:25:41 -07:00
ludovic autin
afa51b4416 lammps is a multiscale simulation engine, coordinate scale option help bringing everything at the properscale in the viewport. I put the option in the OpenFiles state. 2024-10-10 13:21:02 -07:00
ludovic autin
95792dd3c8 Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-10 13:09:00 -07:00
Paul Pillot
e2bc15ac6b Make Loci.isSubset() strict on units comparisons (#1294)
Loci.isSubest could return true for a Loci that is a superset of the reference Loci. This was happening when the second Loci contains units that are not matched in the reference Loci. See #1292
2024-10-10 12:20:41 +02:00
ludovic autin
4e565808c6 more flexible parsing, can handle different atom_style. 2024-10-09 17:21:47 -07:00
ludovic autin
b2e2b46280 Merge remote-tracking branch 'upstream/master' into lammps_data 2024-10-08 11:52:11 -07:00
ludovic autin
462e675237 trailing space 2024-10-08 11:51:35 -07:00
ludovic autin
6e77b4ce71 added my name 2024-10-08 11:29:13 -07:00
ludovic autin
e8bd67c069 lammpstrj string format no boundary coordinates 2024-10-08 11:21:43 -07:00
Yakov Pechersky
fe502539f9 Remove remaining deprecated SASS lighten call (#1291) 2024-10-08 14:38:23 +02:00
ludovic autin
fe5afa8935 rename to lammps_data, so we can have a lammps_traj parser 2024-10-07 17:02:49 -07:00
ludovic autin
20452e762b renaming 2024-10-07 16:45:33 -07:00
ludovic autin
bc2d19338b naive lammps data parser. Lammps data are initial conformation before simulation or are dumped from a trajectory. 2024-10-07 16:11:47 -07:00
giagitom
719e141dd9 Include ssao transparent -> opaque interactions on ssgi 2024-10-07 19:38:34 +02:00
giagitom
5d9d01d251 Merge remote-tracking branch 'upstream/master' into transparent-ssao 2024-10-07 16:02:54 +02:00
Alexander Rose
39ad2f0719 changelog 2024-10-06 16:18:44 -07:00
Alexander Rose
4f06f724a4 Merge pull request #1290 from molstar/transparency-fixes
Transparency related fixes
2024-10-06 16:15:07 -07:00
Alexander Rose
d5a4b266dd wip, supporting transparent ssao with ssgi 2024-10-05 10:44:35 -07:00
Alexander Rose
e1d92a58be fix missing pre-multiplied alpha for blended & wboit with no fog 2024-10-05 10:21:30 -07:00
Alexander Rose
05ff705c25 fix direct-volume for dpoit with fog off and transparent background on 2024-10-05 10:20:40 -07:00
Alexander Rose
f1cfb29a03 renderer cleanup, remove duplicated/unused code 2024-10-05 10:08:44 -07:00
Alexander Rose
d2f354d949 only set depthTextureSupport when given 2024-10-05 10:04:28 -07:00
David Sehnal
481c6926e7 iOS transparency (#1288)
* iOS transparency

* headers

* changelog
2024-10-04 19:29:12 +02:00
giagitom
f15da87e13 Merge remote-tracking branch 'upstream/master' into transparent-ssao 2024-10-03 16:29:39 +02:00
Xavier M
c34aaf7c31 Add doc page to describe how to access component data (#1283) 2024-10-03 13:53:06 +02:00
giagitom
d9f7aafd72 remove unised variable 2024-09-28 15:37:52 +02:00
giagitom
dc7f745dbe Improve postrpocessing frag and fix outlines blending issues 2024-09-26 18:52:58 +02:00
giagitom
4dea8849be Fix dpoit blending issue when postprocessing is off 2024-09-26 11:21:39 +02:00
giagitom
a2056d31bf Improvements and transparency blend mode fix 2024-09-25 22:24:33 +02:00
giagitom
c14344d465 - Removing separatedTransparency option
- Removing includeOpacity option
2024-09-24 12:09:00 +02:00
giagitom
7242494123 Try to handle blended transparency 2024-09-23 12:49:19 +02:00
giagitom
adfea9f336 Merge branch 'master' of https://github.com/molstar/molstar into transparent-ssao 2024-09-23 11:12:02 +02:00
giagitom
1b4d42cc1e Avoid rendering textures used by getMappedDepth when ssao multiScale is off 2024-09-19 19:50:29 +02:00
giagitom
7c8f6255c5 Merge branch 'master' of https://github.com/molstar/molstar into transparent-ssao 2024-09-19 19:09:37 +02:00
giagitom
1ba00c7fa8 Reverting changes 2024-09-05 01:32:19 +02:00
giagitom
1bfc2fe511 Ssao initialization fixes 2024-09-04 12:56:17 +02:00
giagitom
1766fad6f7 Enable ssao on transparency by default 2024-09-02 15:40:29 +02:00
giagitom
d4775812ad Fixes 2024-09-02 15:00:03 +02:00
giagitom
6cf887d44d remove comments 2024-09-02 09:16:56 +02:00
giagitom
bbb2bee2ae changelog 2024-09-01 21:10:39 +02:00
giagitom
73763b444e Merge remote-tracking branch 'upstream/master' into transparent-ssao 2024-09-01 21:07:05 +02:00
giagitom
9508e01e59 Add transparent ssao support 2024-09-01 19:10:59 +02:00
226 changed files with 11248 additions and 2913 deletions

View File

@@ -5,6 +5,95 @@ Note that since we don't clearly distinguish between a public and private interf
## [Unreleased]
- Volume UI improvements
- Render all volume entries instead of selecting them one-by-one
- Toggle visibility of all volumes
- More accessible iso value control
- Support wheel event on sliders
- MolViewSpec extension:
- Add validation for discriminated union params
- Primitives: remove triangle_colors, line_colors, have implicit grouping instead; rename many parameters
- Add `external-structure` theme that colors any geometry by structure properties
- Support float and half-float data type for direct-volume rendering and GPU isosurface extraction
- Minor documentation updates
- Fix plugin mouse interactions when CSS `scale` transform is applied
## [v4.10.0] - 2024-12-15
- Add `ModelWithCoordinates` decorator transform.
- Fix outlines on transparent background using illumination mode (#1364)
- Fix transparent depth texture artifacts using illumination mode
- Fix marking of consecutive gap elements (#876)
- Allow React 19 in dependencies
- Fix missing deflate header if `CompressionStream` is available
- Fix is_iOS check for NodeJS
- Added PluginCommands.Camera.FocusObject
- Plugin state snapshot can have instructions to focus objects (PluginState.Snapshot.camera.focus)
- MolViewSpec extension: Support for multi-state files (animations)
- Fix units transform data not fully updated when structure child changes
- Fix `addIndexPairBonds` quadratic runtime case
- Use adjoint matrix to transform normals in shaders
- Fix resize handling in `tests/browser`
## [v4.9.1] - 2024-12-05
- Fix iOS check when running on Node
## [v4.9.0] - 2024-12-01
- Fix artifacts when using xray shading with high xrayEdgeFalloff values
- Enable double rounded capping on tubular helices
- Fix single residue tubular helices not showing up
- Fix outlines on volume and surface reps that do not disappear (#1326)
- Add example `glb-export`
- Membrane orientation: Improve `isApplicable` check and error handling (#1316)
- Fix set fenceSync to null after deleteSync.
- Fix operator key-based `IndexPairBonds` assignment
- Don't add bonds twice
- Add `IndexPairs.bySameOperator` to avoid looping over all bonds for each unit
- Add `Structure.intraUnitBondMapping`
- Add more structure-based visuals to avoid too many (small) render-objects
- `structure-intra-bond`, `structure-ellipsoid-mesh`, `structure-element-point`, `structure-element-cross`
- Upgrade to express v5 (#1311)
- Fix occupancy check using wrong index for inter-unit bond computation (@rxht, #1321)
- Fix transparent SSAO for image rendering, e.g., volumne slices (#1332)
- Fix bonds not shown with `ignoreHydrogens` on (#1315)
- Better handle mmCIF files with no entities defined by using `label_asym_id`
- Show bonds in water chains when `ignoreHydorgensVariant` is `non-polar`
- Add MembraneServer API, generating data to be consumed in the context of MolViewSpec
- Fix `StructConn.isExhaustive` for partial models (e.g., returned by the model server)
- Refactor value swapping in molstar-math to fix SWC (Next.js) build (#1345)
- Fix transform data not updated when structure child changes
- Fix `PluginStateSnapshotManager.syncCurrent` to work as expected on re-loaded states.
- Fix do not compute implicit hydrogens when unit is explicitly protonated (#1257)
- ModelServer and VolumeServer: support for input files from Google Cloud Storage (gs://)
- Fix color of missing partial charges for SB partial charges extension
## [v4.8.0] - 2024-10-27
- Add SSAO support for transparent geometry
- Fix SSAO color not updating
- Improve blending of overlapping outlines from transparent & opaque geometries
- Default to `blended` transparency on iOS due to `wboit` not being supported.
- Fix direct-volume with fog off (and on with `dpoit`) and transparent background on (#1286)
- Fix missing pre-multiplied alpha for `blended` & `wboit` with no fog (#1284)
- Fix backfaces visible using blended transparency on impostors (#1285)
- Fix StructureElement.Loci.isSubset() only considers common units (#1292)
- Fix `Scene.opacityAverage` calculation never 1
- Fix bloom in illumination mode
- Fix `findPredecessorIndex` bug when repeating values
- MolViewSpec: Support for transparency and custom properties
- MolViewSpec: MVP Support for geometrical primitives (mesh, lines, line, label, distance measurement)
- Mesoscale Explorer: Add support for 4-character PDB IDs (e.g., 8ZZC) in PDB-Dev loader
- Fix Sequence View in Safari 18
- Improve performance of `IndexPairBonds` assignment when operator keys are available
- ModelArchive QualityAssessment extension:
- Add support for ma_qa_metric_local_pairwise mmCIF category
- Add PAE plot component
- Add new AlphaFoldDB-PAE example app
- Add support for LAMMPS data and dump formats
- Remove extra anti-aliasing from text shader (fixes #1208 & #1306)
## [v4.7.1] - 2024-09-30
- Improve `resolutionMode` (#1279)

View File

@@ -876,6 +876,17 @@ ma_qa_metric_local.metric_value
ma_qa_metric_local.model_id
ma_qa_metric_local.ordinal_id
ma_qa_metric_local_pairwise.ordinal_id
ma_qa_metric_local_pairwise.model_id
ma_qa_metric_local_pairwise.label_asym_id_1
ma_qa_metric_local_pairwise.label_comp_id_1
ma_qa_metric_local_pairwise.label_seq_id_1
ma_qa_metric_local_pairwise.label_asym_id_2
ma_qa_metric_local_pairwise.label_comp_id_2
ma_qa_metric_local_pairwise.label_seq_id_2
ma_qa_metric_local_pairwise.metric_id
ma_qa_metric_local_pairwise.metric_value
ma_software_group.group_id
ma_software_group.ordinal_id
ma_software_group.software_id
1 atom_sites.entry_id
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892

View File

@@ -29,7 +29,7 @@ node lib/commonjs/servers/model/server --sourceMap pdb-bcif '/opt/data/bcif/${id
| `--maxQueryManyQueries` | Maximum number of queries allowed by the query-many at a time |
| `--defaultSource` | modifies which 'sourceMap' source to use by default |
| `--sourceMap` | Map `id`s for a `source` to a file path. Example: `pdb-bcif '../../data/bcif/${id}.bcif'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` Can be specified multiple times. The `SOURCE` variable (e.g. `pdb-bcif`) is arbitrary and depends on how you plan to use the server. Supported formats: cif, bcif, cif.gz, bcif.gz |
| `--sourceMapUrl` | Same as `--sourceMap` but for URL. `--sourceMapUrl src url format` Example: `pdb-cif "https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif" cif` Supported formats: cif, bcif, cif.gz, bcif.gz |
| `--sourceMapUrl` | Same as `--sourceMap` but for URL. `--sourceMapUrl src url format` Example: `pdb-cif 'https://www.ebi.ac.uk/pdbe/entry-files/download/${id}_updated.cif' cif` Supported formats: cif, bcif, cif.gz, bcif.gz. Supported protocols: http://, https://, gs:// |
```sh
node lib/commonjs/servers/model/server [-h] [-v]

View File

@@ -66,7 +66,7 @@ To achieve this, use the ``pack`` application (``node lib/commonjs/servers/volum
### Local Mode
The program ``lib/commonjs/servers/volume/pack`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
The program ``lib/commonjs/servers/volume/query`` (``volume-server-query`` in NPM package) can be used to query the data without running a http server.
### Navigating the Source Code
@@ -105,7 +105,7 @@ node lib/commonjs/servers/volume/server --idMap x-ray '/opt/data/xray/${id}.mdb'
| `--defaultPort` | Specify the port the server is running on |
| `--shutdownTimeoutMinutes` | Server will shut down after this amount of minutes, 0 for off. |
| `--shutdownTimeoutVarianceMinutes` | Modifies the shutdown timer by +/- `timeoutVarianceMinutes` (to avoid multiple instances shutting at the same time) |
| `--idMap` | Map `id`s for a `type` to a file path. Example: `x-ray '../../data/mdb/xray/${id}-ccp4.mdb'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` - Can be specified multiple times. - The `TYPE` variable (e.g. `x-ray`) is arbitrary and depends on how you plan to use the server. By default, Mol* Viewer uses `x-ray` and `em`, but any particular use case may vary. |
| `--idMap` | Map `id`s for a `type` to a file path. Example: `x-ray '../../data/mdb/xray/${id}-ccp4.mdb'` - JS expressions can be used inside `${}`, e.g. `${id.substr(1, 2)}/${id}.mdb` - Can be specified multiple times. - The `TYPE` variable (e.g. `x-ray`) is arbitrary and depends on how you plan to use the server. By default, Mol* Viewer uses `x-ray` and `em`, but any particular use case may vary. - If using URL, it can be http://, https://, gs:// or file:// protocol.|
| `--maxRequestBlockCount` | Maximum number of blocks that could be read in 1 query. This is somewhat tied to the ``maxOutputSizeInVoxelCountByPrecisionLevel`` in that the `&lt;maximum number of voxel&gt; = maxRequestBlockCount * &lt;block size&gt;^3`. The default block size is 96 which corresponds to 28,311,552 voxels with 32 max blocks. |
| `--maxFractionalBoxVolume` | The maximum fractional volume of the query box (to prevent queries that are too big). |
| `--maxOutputSizeInVoxelCountByPrecisionLevel` | What is the (approximate) maximum desired size in voxel count by precision level - Rule of thumb: `&lt;response gzipped size&gt;` in `[&lt;voxel count&gt; / 8, &lt;voxel count&gt; / 4]`. The maximum number of voxels is tied to maxRequestBlockCount. |

View File

@@ -0,0 +1,59 @@
# Exporting components
Export components data can be useful to reproduce the same view in a different visualization software.
To do that, one would need to loop over all components, extract its selection (for example by using atom indices) and its representations (type, coloring and sizing).
### Getting assets / molecular files
```js
for (const { asset, file } of plugin.managers.asset.assets) {
const isFile = asset.asset.kind === 'url'
console.log(asset.asset.id)
console.log(isFile)
const data = await file.arrayBuffer()
}
```
### Getting components per structure
```js
import { PluginStateObject as PSO } from 'molstar/lib/mol-plugin-state/objects';
//...
const componentManager = plugin.managers.structure.component;
for (const structure of componentManager.currentStructures) {
if (!structure.properties) {
continue;
}
const cell = plugin.state.data.select(structure.properties.cell.transform.ref)[0];
if (!cell || !cell.obj) {
continue;
}
const structureData = (cell.obj as PSO.Molecule.Structure).data;
for (const component of structure.components) {
if (!component.cell.obj) {
continue;
}
// For each component in each structure, display the content of the selection
Structure.eachAtomicHierarchyElement(component.cell.obj.data, {
atom: location => console.log(location.element)
});
for (const rep of component.representations) {
// For each representation of the component, display its type
console.log(rep.cell?.transform?.params?.type?.name)
// Also display the color for each atom
const colorThemeName = rep.cell.transform.params?.colorTheme.name;
const colorThemeParams = rep.cell.transform.params?.colorTheme.params;
const theme = plugin.representation.structure.themes.colorThemeRegistry.create(
colorThemeName || '',
{ structure: structureData },
colorThemeParams
) as ColorTheme<typeof colorThemeParams>;
Structure.eachAtomicHierarchyElement(component.cell.obj.data, {
atom: loc => console.log(theme.color(loc, false))
});
}
}
}
```

View File

@@ -6,7 +6,7 @@
What is a plugin? A plugin is a collection of modules that provide functionality to the `Mol*` UI. The plugin is responsible for managing the state of the viewer, internal and user interactions. It has been a previous point of confusion for new users of `Mol*` to associate the __viewer__ part of the library with what is further referred to as the __plugin__. These two are closely connected in the `molstar-plugin-ui` module, which is the user-facing part of the library and ultimately provides the viewer, but they are ultimately distinct.
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
It is recommended that you inspect the general class structure of [`PluginInitWrapper`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/plugin.tsx#L41), [`PluginUIContext`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/context.ts#L12) and [`PluginUIComponent`](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin-ui/base.tsx#L16) to better understand the flow of data and events in the plugin.
A passing analogy is that a [ `PluginContext` ](https://github.com/molstar/molstar/blob/6edbae80db340134341631f669eec86543a0f1a8/src/mol-plugin/context.ts#L71) is the engine that powers computation, rendering, events and subscriptions inside the molstar UI. All UI components depend on `PluginContext`.

View File

@@ -0,0 +1,45 @@
# Assign custom conformation to a Model
This document shows how to update model conformation dynamically using the `ModelWithCoordinates` transforms. If this does not work well with your particular use case, it is suggested to write a custom version of `ModelWithCoordinates` with similar usage as outlined in this document.
```ts
async function animateFirstXCoordinateExample(plugin: PluginContext, url: string, format: BuiltInTrajectoryFormat) {
// Load data
const _data = await plugin.builders.data.download({ url });
const trajectory = await plugin.builders.structure.parseTrajectory(_data, format);
const hierarchy = await this.plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
if (!hierarchy) return;
// Insert ModelWithCoordinates cell to be updated in the loop bellow
const coordinatesNode = await plugin.build().to(hierarchy!.model).insert(ModelWithCoordinates).commit();
const x0 = hierarchy!.model.data!.atomicConformation.x[0];
let xOffset = 0;
async function animateFirstXCoord() {
// Normally, the whole conformation would come from an API/library call, but here we fake it:
const { x, y, z } = hierarchy!.model.data!.atomicConformation;
const nextX = [...(x as number[])];
nextX[0] = x0 + xOffset;
xOffset += 0.05;
if (xOffset > 1) xOffset = 0;
// Construct new coodinate frame from the data and commit the update.
// Rest of the state tree will reconcile automatically.
await plugin.build().to(coordinatesNode).update({
atomicCoordinateFrame: {
elementCount: x.length,
time: { value: 0, unit: 'step' },
xyzOrdering: { isIdentity: true },
x: nextX,
y,
z,
}
}).commit();
requestAnimationFrame(animateFirstXCoord);
}
animateFirstXCoord();
}
// animateFirstXCoordinateExample('https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/CID/2244/record/SDF/?record_type=3d', 'sdf');
```

View File

@@ -40,6 +40,7 @@ nav:
- CIF Schemas: 'plugin/cif-schemas.md'
- State Transforms:
- Custom Trajectory: 'plugin/transforms/custom-trajectory.md'
- Custom Conformation: 'plugin/transforms/custom-conformation.md'
- Data Access Tools:
- 'data-access-tools/model-server.md'
- Volume Server:
@@ -57,4 +58,5 @@ nav:
- Tunnels: 'extensions/tunnels.md'
- Misc:
- Interesting PDB entries: misc/interesting-pdb-entries.md
repo_url: https://github.com/molstar/docs
- Exporting component data: exporting-components.md
repo_url: https://github.com/molstar/docs

3056
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "molstar",
"version": "4.7.1",
"version": "4.10.0",
"description": "A comprehensive macromolecular library.",
"homepage": "https://github.com/molstar/molstar#readme",
"repository": {
@@ -111,22 +111,25 @@
"Neli Fonseca <neli@ebi.ac.uk>",
"Paul Pillot <paul.pillot@tandemai.com>",
"Herman Bergwerf <post@hbergwerf.nl>",
"Eric E <etongfu@outlook.com>"
"Eric E <etongfu@outlook.com>",
"Xavier Martinez <xavier.martinez.xm@gmail.com>",
"Alex Chan <smalldirkalex@gmail.com>",
"Simeon Borko <simeon.borko@gmail.com>"
],
"license": "MIT",
"devDependencies": {
"@types/cors": "^2.8.17",
"@types/gl": "^6.0.5",
"@types/jest": "^29.5.13",
"@types/jest": "^29.5.14",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.10",
"@types/react-dom": "^18.3.0",
"@types/react": "^18.3.16",
"@types/react-dom": "^18.3.5",
"@typescript-eslint/eslint-plugin": "^7.18.0",
"@typescript-eslint/parser": "^7.18.0",
"benchmark": "^2.1.4",
"concurrently": "^9.0.1",
"cpx2": "^7.0.1",
"crypto-browserify": "^3.12.0",
"concurrently": "^9.1.0",
"cpx2": "^8.0.0",
"crypto-browserify": "^3.12.1",
"css-loader": "^7.1.2",
"eslint": "^8.57.1",
"extra-watch-webpack-plugin": "^1.0.3",
@@ -135,55 +138,58 @@
"http-server": "^14.1.1",
"jest": "^29.7.0",
"jpeg-js": "^0.4.4",
"mini-css-extract-plugin": "^2.9.1",
"mini-css-extract-plugin": "^2.9.2",
"path-browserify": "^1.0.1",
"raw-loader": "^4.0.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"sass": "^1.79.4",
"sass-loader": "^16.0.2",
"sass": "^1.83.0",
"sass-loader": "^16.0.4",
"simple-git": "^3.27.0",
"stream-browserify": "^3.0.0",
"style-loader": "^4.0.0",
"ts-jest": "^29.2.5",
"typescript": "^5.6.2",
"webpack": "^5.95.0",
"typescript": "^5.7.2",
"webpack": "^5.97.1",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@types/argparse": "^2.0.16",
"@types/argparse": "^2.0.17",
"@types/benchmark": "^2.1.5",
"@types/compression": "1.7.5",
"@types/express": "^4.17.21",
"@types/node": "^18.19.54",
"@types/node-fetch": "^2.6.11",
"@types/express": "^5.0.0",
"@types/node": "^18.19.68",
"@types/node-fetch": "^2.6.12",
"@types/swagger-ui-dist": "3.30.5",
"argparse": "^2.0.1",
"body-parser": "^1.20.3",
"compression": "^1.7.4",
"compression": "^1.7.5",
"cors": "^2.8.5",
"express": "^4.21.0",
"express": "^5.0.1",
"h264-mp4-encoder": "^1.0.12",
"immer": "^10.1.1",
"immutable": "^4.3.7",
"io-ts": "^2.2.21",
"immutable": "^5.0.3",
"io-ts": "^2.2.22",
"node-fetch": "^2.7.0",
"react-markdown": "^9.0.1",
"rxjs": "^7.8.1",
"swagger-ui-dist": "^5.17.14",
"tslib": "^2.7.0",
"swagger-ui-dist": "^5.18.2",
"tslib": "^2.8.1",
"util.promisify": "^1.1.2",
"xhr2": "^0.2.1"
},
"peerDependencies": {
"@google-cloud/storage": "^7.14.0",
"canvas": "^2.11.2",
"gl": "^6.0.2",
"jpeg-js": "^0.4.4",
"pngjs": "^6.0.0",
"react": "^18.1.0 || ^17.0.2 || ^16.14.0",
"react-dom": "^18.1.0 || ^17.0.2 || ^16.14.0"
"react": ">=16.14.0",
"react-dom": ">=16.14.0"
},
"peerDependenciesMeta": {
"@google-cloud/storage": {
"optional": true
},
"canvas": {
"optional": true
},

View File

@@ -61,9 +61,13 @@ function copyDemos() {
}
function copyFiles() {
copyViewer();
copyMe();
copyDemos();
try {
copyViewer();
copyMe();
copyDemos();
} catch (e) {
console.error(e);
}
}
if (!fs.existsSync(localPath)) {

View File

@@ -44,16 +44,6 @@ function occlusionStyle(plugin: PluginContext) {
},
postprocessing: {
...plugin.canvas3d!.props.postprocessing,
occlusion: { name: 'on', params: {
blurKernelSize: 15,
blurDepthBias: 0.5,
multiScale: { name: 'off', params: {} },
radius: 5,
bias: 0.8,
samples: 32,
resolutionScale: 1,
color: Color(0x000000),
} },
outline: { name: 'on', params: {
scale: 1.0,
threshold: 0.33,

View File

@@ -394,7 +394,10 @@ export function MesoViewportSnapshotDescription() {
{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>}
{e.descriptionFormat === 'plaintext'
&& e.description
|| <Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>
}
</div>
</>
);

View File

@@ -84,6 +84,7 @@ function adjustPluginProps(ctx: PluginContext) {
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
transparentThreshold: 0.4,
}
},
shadow: {
@@ -212,8 +213,14 @@ export async function loadPdb(ctx: PluginContext, id: string) {
export async function loadPdbDev(ctx: PluginContext, id: string) {
await reset(ctx);
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
const url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
let url: string;
// 4 character PDB id, TODO: support extended PDB ID
if (id.match(/^[1-9][A-Z0-9]{3}$/i) !== null) {
url = `https://pdb-dev.wwpdb.org/bcif/${id.toLowerCase()}.bcif`;
} else {
const nId = id.toUpperCase().startsWith('PDBDEV_') ? id : `PDBDEV_${id.padStart(8, '0')}`;
url = `https://pdb-dev.wwpdb.org/bcif/${nId.toUpperCase()}.bcif`;
}
const data = await ctx.builders.data.download({ url, isBinary: true });
await createHierarchy(ctx, data.ref);
}

View File

@@ -4,6 +4,7 @@
* @author David Sehnal <david.sehnal@gmail.com>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author Neli Fonseca <neli@ebi.ac.uk>
* @author Adam Midlik <midlik@gmail.com>
*/
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
@@ -11,13 +12,13 @@ import { Backgrounds } from '../../extensions/backgrounds';
import { DnatcoNtCs } from '../../extensions/dnatco';
import { G3DFormat, G3dProvider } from '../../extensions/g3d/format';
import { GeometryExport } from '../../extensions/geo-export';
import { MAQualityAssessment, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { MAQualityAssessment, MAQualityAssessmentConfig, QualityAssessmentPLDDTPreset, QualityAssessmentQmeanPreset } from '../../extensions/model-archive/quality-assessment/behavior';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { ModelExport } from '../../extensions/model-export';
import { Mp4Export } from '../../extensions/mp4-export';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS } from '../../extensions/mvs/load';
import { loadMVS, MolstarLoadingExtension } from '../../extensions/mvs/load';
import { MVSData } from '../../extensions/mvs/mvs-data';
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
import { RCSBValidationReport } from '../../extensions/rcsb';
@@ -47,7 +48,7 @@ import { createPluginUI } from '../../mol-plugin-ui';
import { renderReact18 } from '../../mol-plugin-ui/react18';
import { DefaultPluginUISpec, PluginUISpec } from '../../mol-plugin-ui/spec';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginConfig } from '../../mol-plugin/config';
import { PluginConfig, PluginConfigItem } from '../../mol-plugin/config';
import { PluginLayoutControlsDisplay } from '../../mol-plugin/layout';
import { PluginSpec } from '../../mol-plugin/spec';
import { PluginState } from '../../mol-plugin/state';
@@ -124,6 +125,8 @@ const DefaultViewerOptions = {
rcsbAssemblySymmetryDefaultServerType: AssemblySymmetryConfig.DefaultServerType.defaultValue,
rcsbAssemblySymmetryDefaultServerUrl: AssemblySymmetryConfig.DefaultServerUrl.defaultValue,
rcsbAssemblySymmetryApplyColors: AssemblySymmetryConfig.ApplyColors.defaultValue,
config: [] as [PluginConfigItem, any][],
};
type ViewerOptions = typeof DefaultViewerOptions;
@@ -203,6 +206,7 @@ export class Viewer {
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
...(o.config ?? []),
]
};
@@ -507,7 +511,7 @@ export class Viewer {
return { model, coords, preset };
}
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
async loadMvsFromUrl(url: string, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
if (format === 'mvsj') {
const data = await this.plugin.runTask(this.plugin.fetch({ url, type: 'string' }));
const mvsData = MVSData.fromMVSJ(data);
@@ -526,7 +530,7 @@ export class Viewer {
/** Load MolViewSpec from `data`.
* If `format` is 'mvsj', `data` must be a string or a Uint8Array containing a UTF8-encoded string.
* If `format` is 'mvsx', `data` must be a Uint8Array or a string containing base64-encoded binary data prefixed with 'base64,'. */
async loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
async loadMvsData(data: string | Uint8Array, format: 'mvsj' | 'mvsx', options?: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
if (typeof data === 'string' && data.startsWith('base64')) {
data = Uint8Array.from(atob(data.substring(7)), c => c.charCodeAt(0)); // Decode base64 string to Uint8Array
}
@@ -615,4 +619,9 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
export const PluginExtensions = {
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
mvs: { MVSData, loadMVS },
modelArchive: {
qualityAssessment: {
config: MAQualityAssessmentConfig
}
}
};

View File

@@ -12,7 +12,6 @@
import { ArgumentParser } from 'argparse';
import { treeSchemaToMarkdown, treeSchemaToString } from '../../extensions/mvs/tree/generic/tree-schema';
import { MVSDefaults } from '../../extensions/mvs/tree/mvs/mvs-defaults';
import { MVSTreeSchema } from '../../extensions/mvs/tree/mvs/mvs-tree';
@@ -32,9 +31,9 @@ function parseArguments(): Args {
/** Main workflow for printing MolViewSpec tree schema. */
function main(args: Args) {
if (args.markdown) {
console.log(treeSchemaToMarkdown(MVSTreeSchema, MVSDefaults));
console.log(treeSchemaToMarkdown(MVSTreeSchema));
} else {
console.log(treeSchemaToString(MVSTreeSchema, MVSDefaults));
console.log(treeSchemaToString(MVSTreeSchema));
}
}

View File

@@ -5,9 +5,14 @@
* @author Adam Midlik <midlik@gmail.com>
*
* Command-line application for rendering images from MolViewSpec files
* Build: npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
* From Molstar NPM package:
* npm install molstar canvas gl jpeg-js pngjs
* npx mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
* From Molstar source code:
* npm install
* npm install --no-save canvas gl jpeg-js pngjs // these packages are not listed in Mol* dependencies for performance reasons
* npm run build
* node lib/commonjs/cli/mvs/mvs-render -i examples/mvs/1cbs.mvsj -o ../outputs/1cbs.png --size 800x600 --molj
*/
import { ArgumentParser } from 'argparse';
@@ -29,6 +34,7 @@ import { onelinerJsonString } from '../../mol-util/json';
import { ParamDefinition as PD } from '../../mol-util/param-definition';
// MolViewSpec must be imported after HeadlessPluginContext
import { Mp4Export } from '../../extensions/mp4-export';
import { MolViewSpec } from '../../extensions/mvs/behavior';
import { loadMVSX } from '../../extensions/mvs/components/formats';
import { loadMVS } from '../../extensions/mvs/load';
@@ -46,6 +52,7 @@ interface Args {
output: string[],
size: { width: number, height: number },
molj: boolean,
no_extensions: boolean,
}
/** Return parsed command line arguments for `main` */
@@ -55,6 +62,7 @@ function parseArguments(): Args {
parser.add_argument('-o', '--output', { required: true, nargs: '+', help: 'File path(s) for output files (one output path for each input file). Output format is inferred from the file extension (.png or .jpg)' });
parser.add_argument('-s', '--size', { help: `Output image resolution, {width}x{height}. Default: ${DEFAULT_SIZE}.`, default: DEFAULT_SIZE });
parser.add_argument('-m', '--molj', { action: 'store_true', help: `Save Mol* state (.molj) in addition to rendered images (use the same output file paths but with .molj extension)` });
parser.add_argument('-n', '--no-extensions', { action: 'store_true', help: `Do not apply builtin MVS-loading extensions (not a part of standard MVS specification)` });
const args = parser.parse_args();
try {
const parts = args.size.split('x');
@@ -92,13 +100,17 @@ async function main(args: Args): Promise<void> {
} else {
throw new Error(`Input file name must end with .mvsj or .mvsx: ${input}`);
}
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: sourceUrl });
await loadMVS(plugin, mvsData, { sanityChecks: true, replaceExisting: true, sourceUrl: sourceUrl, extensions: args.no_extensions ? [] : undefined });
fs.mkdirSync(path.dirname(output), { recursive: true });
if (args.molj) {
await plugin.saveStateSnapshot(withExtension(output, '.molj'));
}
await plugin.saveImage(output);
if (output.toLowerCase().endsWith('.mp4')) {
await plugin.saveAnimation(output);
} else {
await plugin.saveImage(output);
}
checkState(plugin);
}
await plugin.clear();
@@ -110,6 +122,7 @@ async function createHeadlessPlugin(args: Pick<Args, 'size'>): Promise<HeadlessP
const externalModules: ExternalModules = { gl, pngjs, 'jpeg-js': jpegjs };
const spec = DefaultPluginSpec();
spec.behaviors.push(PluginSpec.Behavior(MolViewSpec));
spec.behaviors.push(PluginSpec.Behavior(Mp4Export));
const headlessCanvasOptions = defaultCanvas3DParams();
const canvasOptions = {
...PD.getDefaultValues(Canvas3DParams),

View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Mol* AlphaFold DB Predicted Aligned Error Example</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: sans-serif;
}
#app {
position: absolute;
top: 20px;
left: 20px;
width: 640px;
height: 480px;
}
#plot {
position: absolute;
left: 680px;
top: 20px;
width: 480px;
height: 480px;
}
#controls {
position: absolute;
left: 20px;
top: 520px;
font-family: sans-serif;
font-size: smaller;
}
</style>
<link rel="stylesheet" type="text/css" href="molstar.css" />
<script type="text/javascript" src="./index.js"></script>
</head>
<body>
<div id='controls'>
<input type='text' id='af-id' value='Q8W3K0' />
<button id='af-load'>Load</button>
</div>
<div id='app'></div>
<div id='plot'></div>
<script>
AlphaFoldPAEExample.init({ pluginContainerId: 'app', plotContainerId: 'plot' }).then(example => {
example.load('Q8W3K0')
});
function $(id) { return document.getElementById(id); }
$('af-load').onclick = () => AlphaFoldPAEExample.load($('af-id').value)
</script>
<!-- __MOLSTAR_ANALYTICS__ -->
</body>
</html>

View File

@@ -0,0 +1,96 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { createRoot } from 'react-dom/client';
import { Viewer } from '../../apps/viewer/app';
import { MAPairwiseScorePlot } from '../../extensions/model-archive/quality-assessment/pairwise/ui';
import { QualityAssessment } from '../../extensions/model-archive/quality-assessment/prop';
import { Model, ResidueIndex } from '../../mol-model/structure';
import './index.html';
require('mol-plugin-ui/skin/light.scss');
export class AlphaFoldPAEExample {
viewer: Viewer;
plotContainerId: string;
async init(options: { pluginContainerId: string, plotContainerId: string }) {
this.plotContainerId = options.plotContainerId;
this.viewer = await Viewer.create(options.pluginContainerId, {
layoutIsExpanded: false,
layoutShowControls: false,
layoutShowLeftPanel: false,
layoutShowLog: false,
});
return this;
}
async load(afId: string) {
const id = afId.trim().toUpperCase();
const plotRoot = createRoot(document.getElementById(this.plotContainerId)!);
plotRoot.render(<div>Loading...</div>);
await this.viewer.plugin.clear();
await this.viewer.loadAlphaFoldDb(id);
try {
const req = await fetch(`https://alphafold.ebi.ac.uk/files/AF-${id}-F1-predicted_aligned_error_v4.json`);
const json = await req.json();
const model = this.viewer.plugin.managers.structure.hierarchy.current.models[0]?.cell.obj?.data!;
const metric = pairwiseMetricFromAlphaFoldDbJson(model, json)!;
createRoot(document.getElementById(this.plotContainerId)!).render(
<div className='msp-plugin' style={{ background: 'white' }}>
<MAPairwiseScorePlot plugin={this.viewer.plugin} pairwiseMetric={metric} model={model} />
</div>
);
} catch (err) {
plotRoot.render(<div>Error: {String(err)}</div>);
}
}
}
function pairwiseMetricFromAlphaFoldDbJson(model: Model, data: any): QualityAssessment.Pairwise | undefined {
if (!Array.isArray(data) || !data[0]?.predicted_aligned_error) return undefined;
const { residues, residueAtomSegments, atomSourceIndex } = model.atomicHierarchy;
const sortedResidueIndices = new Array(residues._rowCount).fill(0).map((_, i) => i);
sortedResidueIndices.sort((a, b) => {
const idxA = atomSourceIndex.value(residueAtomSegments.offsets[a]);
const idxB = atomSourceIndex.value(residueAtomSegments.offsets[b]);
return idxA - idxB;
});
const metricData = data[0].predicted_aligned_error as number[][];
const metric: QualityAssessment.Pairwise = {
id: 0,
name: 'AlphaFold DB PAE',
residueRange: [0 as ResidueIndex, (residues._rowCount - 1) as ResidueIndex],
valueRange: [0, data[0].max_predicted_aligned_error],
values: {}
};
for (let i = 0; i < metricData.length; i++) {
const rA = sortedResidueIndices[i];
if (typeof rA !== 'number') continue;
const row = metricData[i];
const xs: any = (metric.values[rA as ResidueIndex] = {});
for (let j = 0; j < row.length; j++) {
const rB = sortedResidueIndices[j];
if (typeof rB !== 'number') continue;
xs[rB] = row[j];
}
}
return metric;
}
(window as any).AlphaFoldPAEExample = new AlphaFoldPAEExample();

View File

@@ -0,0 +1,97 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alex Chan <smalldirkalex@gmail.com>
*
* Thanks to @author Adam Midlik <midlik@gmail.com> for the example code ../image-renderer and https://github.com/midlik/surface-calculator i can make reference to,
*
* Example command-line application generating and exporting PubChem SDF structures
* Build: npm install --no-save gl // these packages are not listed in dependencies for performance reasons
* npm run build
* Run: node lib/commonjs/examples/glb-export 2519 ../outputs_2519/
*/
import { ArgumentParser } from 'argparse';
import fs from 'fs';
import path from 'path';
import gl from 'gl';
import { Task } from '../../mol-task';
import { Download } from '../../mol-plugin-state/transforms/data';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { GlbExporter } from '../../extensions/geo-export/glb-exporter';
import { Box3D } from '../../mol-math/geometry';
import { ModelFromTrajectory, StructureFromModel, TrajectoryFromSDF } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { HeadlessPluginContext } from '../../mol-plugin/headless-plugin-context';
import { DefaultPluginSpec } from '../../mol-plugin/spec';
import { ExternalModules } from '../../mol-plugin/util/headless-screenshot';
import { setFSModule } from '../../mol-util/data-source';
setFSModule(fs);
// cid `2519` for Caffeine
interface Args {
cid: string,
outDirectory: string
}
function parseArguments(): Args {
const parser = new ArgumentParser({ description: 'Example command-line application exporting .glb file of SDF structures from PubChem' });
parser.add_argument('cid', { help: 'PubChem identifier' });
parser.add_argument('outDirectory', { help: 'Directory for outputs' });
const args = parser.parse_args();
return { ...args };
}
async function main() {
const args = parseArguments();
const root = 'https://pubchem.ncbi.nlm.nih.gov/rest';
const url = `${root}/pug/compound/cid/${args.cid}/sdf?record_type=3d`;
console.log('PubChem CID:', args.cid);
console.log('Source URL:', url);
console.log('Outputs:', args.outDirectory);
// Create a headless plugin
const externalModules: ExternalModules = { gl };
const plugin = new HeadlessPluginContext(externalModules, DefaultPluginSpec());
await plugin.init();
// Download and visualize data in the plugin
const update = plugin.build();
const structure = await update.toRoot()
.apply(Download, { url, isBinary: false })
.apply(TrajectoryFromSDF)
.apply(ModelFromTrajectory)
.apply(StructureFromModel)
.apply(StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { size: 'physical' } },
colorTheme: { name: 'element-symbol', params: { carbonColor: { name: 'element-symbol', params: {} } } },
sizeTheme: { name: 'physical', params: {} },
})
.commit();
const meshes = structure.data!.repr.renderObjects.filter(obj => obj.type === 'mesh') as GraphicsRenderObject<'mesh'>[];
const boundingSphere = plugin.canvas3d?.boundingSphereVisible!;
const boundingBox = Box3D.fromSphere3D(Box3D(), boundingSphere);
const renderObjectExporter = new GlbExporter(boundingBox);
await plugin.runTask(Task.create('Export Geometry', async ctx => {
for (let i = 0, il = meshes.length; i < il; ++i) {
await renderObjectExporter.add(meshes[i], plugin.canvas3d?.webgl!, ctx);
}
const blob = await renderObjectExporter.getBlob(ctx);
const buffer = await blob.arrayBuffer();
await fs.promises.writeFile(path.join(args.outDirectory, `${args.cid}.glb`), Buffer.from(buffer));
}));
// Cleanup
await plugin.clear();
plugin.dispose();
}
main();

View File

@@ -36,6 +36,7 @@ const Canvas3DPresets = {
blurDepthBias: 0.5,
resolutionScale: 1,
color: Color(0x000000),
transparentThreshold: 0.4,
}
},
outline: {

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2020 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 Sebastian Bittrich <sebastian.bittrich@rcsb.org>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -67,13 +67,33 @@ export const MembraneOrientationProvider: CustomStructureProperty.Provider<Membr
type: 'root',
defaultParams: MembraneOrientationParams,
getParams: (data: Structure) => MembraneOrientationParams,
isApplicable: (data: Structure) => true,
isApplicable,
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<MembraneOrientationProps>) => {
const p = { ...PD.getDefaultValues(MembraneOrientationParams), ...props };
return { value: await computeAnvil(ctx, data, p) };
try {
return { value: await computeAnvil(ctx, data, p) };
} catch (e) {
// the "Residues Embedded in Membrane" symbol may bypass isApplicable() checks
console.warn('Failed to predict membrane orientation. This happens for short peptides and entries without amino acids.');
return { value: undefined };
}
}
});
function isApplicable(structure: Structure) {
if (!structure.isAtomic) return false;
for (const model of structure.models) {
const { byEntityKey } = model.sequence;
for (const key of Object.keys(byEntityKey)) {
const { kind, length } = byEntityKey[+key].sequence;
if (kind !== 'protein') continue; // can only process protein chains
if (length >= 15) return true; // short peptides might fail
}
}
return false;
}
async function computeAnvil(ctx: CustomProperty.Context, data: Structure, props: Partial<ANVILProps>): Promise<MembraneOrientation> {
const p = { ...PD.getDefaultValues(ANVILParams), ...props };
return await computeANVIL(data, p).runInContext(ctx.runtime);

View File

@@ -81,7 +81,7 @@ export const MembraneOrientationRepresentationProvider = StructureRepresentation
defaultValues: PD.getDefaultValues(MembraneOrientationParams),
defaultColorTheme: { name: 'shape-group' },
defaultSizeTheme: { name: 'shape-group' },
isApplicable: (structure: Structure) => structure.elementCount > 0,
isApplicable(structure: Structure) { return MembraneOrientationProvider.isApplicable(structure); },
ensureCustomProperties: {
attach: (ctx: CustomProperty.Context, structure: Structure) => MembraneOrientationProvider.attach(ctx, structure, void 0, true),
detach: (data) => MembraneOrientationProvider.ref(data, false)

View File

@@ -1,7 +1,8 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
@@ -17,6 +18,12 @@ import { cantorPairing } from '../../../mol-data/util';
import { QmeanScoreColorThemeProvider } from './color/qmean';
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
import { StateObjectRef } from '../../../mol-state';
import { MAPairwiseScorePlotPanel } from './pairwise/ui';
import { PluginConfigItem } from '../../../mol-plugin/config';
export const MAQualityAssessmentConfig = {
EnablePairwiseScorePlot: new PluginConfigItem('ma-quality-assessment-prop.enable-pairwise-score-plot', true),
};
export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean, showTooltip: boolean }>({
name: 'ma-quality-assessment-prop',
@@ -52,6 +59,10 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentPLDDTPreset);
this.ctx.builders.structure.representation.registerPreset(QualityAssessmentQmeanPreset);
if (this.ctx.config.get(MAQualityAssessmentConfig.EnablePairwiseScorePlot)) {
this.ctx.customStructureControls.set('ma-quality-assessment-pairwise-plot', MAPairwiseScorePlotPanel as any);
}
}
update(p: { autoAttach: boolean, showTooltip: boolean }) {
@@ -76,6 +87,8 @@ export const MAQualityAssessment = PluginBehavior.create<{ autoAttach: boolean,
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentPLDDTPreset);
this.ctx.builders.structure.representation.unregisterPreset(QualityAssessmentQmeanPreset);
this.ctx.customStructureControls.delete('ma-quality-assessment-pairwise-plot');
}
},
params: () => ({

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Model, ResidueIndex } from '../../../../mol-model/structure';
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
import { Color } from '../../../../mol-util/color';
import { QualityAssessment } from '../prop';
const DefaultMetricColorRange = [0x00441B, 0xF7FCF5] as [Color, Color];
export type MAResidueRangeInfo = { startOffset: number, endOffset: number, label: string };
function drawMetricPNG(model: Model, metric: QualityAssessment.Pairwise, colorRange: [Color, Color], noDataColor: Color) {
const [minResidueIndex, maxResidueIndex] = metric.residueRange;
const [minMetric, maxMetric] = metric.valueRange;
const [minColor, maxColor] = colorRange;
const range = maxResidueIndex - minResidueIndex + 1;
const valueRange = maxMetric - minMetric;
const values = metric.values;
const canvas = document.createElement('canvas');
canvas.width = range;
canvas.height = range;
const ctx = canvas.getContext('2d')!;
ctx.fillStyle = Color.toStyle(noDataColor);
ctx.fillRect(0, 0, canvas.width, canvas.height);
for (let rA = minResidueIndex; rA <= maxResidueIndex; rA++) {
const row = values[rA];
if (!row) continue;
for (let rB = minResidueIndex; rB <= maxResidueIndex; rB++) {
const value = row[rB];
if (typeof value !== 'number') continue;
const x = rA - minResidueIndex;
const y = rB - minResidueIndex;
const t = (value - minMetric) / valueRange;
const color = Color.interpolate(minColor, maxColor, t);
ctx.fillStyle = Color.toStyle(color);
ctx.fillRect(x, y, 1, 1);
ctx.fillRect(y, x, 1, 1);
}
}
const chains: MAResidueRangeInfo[] = [];
const hierarchy = model.atomicHierarchy;
const { label_asym_id } = hierarchy.chains;
let cI = AtomicHierarchy.residueChainIndex(hierarchy, minResidueIndex as ResidueIndex);
let currentChain: MAResidueRangeInfo = { startOffset: 0, endOffset: 1, label: label_asym_id.value(cI) };
chains.push(currentChain);
for (let i = 1; i < range; i++) {
cI = AtomicHierarchy.residueChainIndex(hierarchy, (minResidueIndex + i) as ResidueIndex);
const asym_id = label_asym_id.value(cI);
if (asym_id === currentChain.label) {
currentChain.endOffset = i + 1;
} else {
currentChain = { startOffset: i, endOffset: i + 1, label: asym_id };
chains.push(currentChain);
}
}
return {
model,
metric,
chains,
colorRange: [Color.toStyle(colorRange[0]), Color.toStyle(colorRange[1])] as const,
png: canvas.toDataURL('png')
};
}
export function maDrawPairwiseMetricPNG(model: Model, metric: QualityAssessment.Pairwise) {
return drawMetricPNG(model, metric, DefaultMetricColorRange, Color(0xE2E2E2));
}
export type MAPairwiseMetricDrawing = ReturnType<typeof drawMetricPNG>

View File

@@ -0,0 +1,504 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { CSSProperties, Fragment, memo, ReactNode, useEffect, useRef } from 'react';
import { BehaviorSubject, combineLatest, distinctUntilChanged, throttleTime } from 'rxjs';
import { clamp } from '../../../../mol-math/interpolate';
import { Model, ResidueIndex, StructureElement, StructureProperties, StructureQuery } from '../../../../mol-model/structure';
import { AtomicHierarchy } from '../../../../mol-model/structure/model/properties/atomic';
import { atoms } from '../../../../mol-model/structure/query/queries/generators';
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
import { OverpaintStructureRepresentation3DFromBundle } from '../../../../mol-plugin-state/transforms/representation';
import { CollapsableControls, CollapsableState } from '../../../../mol-plugin-ui/base';
import { ScatterPlotSvg } from '../../../../mol-plugin-ui/controls/icons';
import { ParameterControls } from '../../../../mol-plugin-ui/controls/parameters';
import { useBehavior } from '../../../../mol-plugin-ui/hooks/use-behavior';
import { PluginContext } from '../../../../mol-plugin/context';
import { StateBuilder, StateTransform } from '../../../../mol-state';
import { round } from '../../../../mol-util';
import { Color } from '../../../../mol-util/color';
import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
import { QualityAssessment } from '../prop';
import { maDrawPairwiseMetricPNG, MAPairwiseMetricDrawing } from './plot';
type State = ReturnType<typeof getPropsAndValues>
export class MAPairwiseScorePlotPanel extends CollapsableControls<{}, State> {
protected defaultState(): State & CollapsableState {
return {
header: 'Predicted Aligned Error',
isCollapsed: false,
isHidden: true,
brand: { accent: 'purple', svg: ScatterPlotSvg },
params: {} as any,
values: undefined as any,
dataSources: [],
};
}
toggleCollapsed() {
if (!this.state.isCollapsed) {
this.setState({ isCollapsed: true });
} else {
const state = getPropsAndValues(this.plugin, this.state.values);
this.setState({
...state,
isCollapsed: false,
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
});
}
};
interactivity = new BehaviorSubject<PlotInteractivityState>({});
queue = new SingleAsyncQueue();
componentDidMount() {
this.subscribe(combineLatest([
this.plugin.state.data.events.changed,
this.plugin.behaviors.state.isAnimating
]), ([_, anim]) => {
if (anim || this.state.isCollapsed) return;
const state = getPropsAndValues(this.plugin, this.state.values);
this.setState({
...state,
isHidden: state.params.data.options.length === 0 || state.params.model.options.length === 0
});
});
this.subscribe(filterHighlightState(this.interactivity), state => {
highlightState(this.plugin, state);
});
this.subscribe(filterOverpaintState(this.interactivity), state => {
this.queue.enqueue(() => overpaintState(this.plugin, state));
});
}
protected renderControls(): JSX.Element | null {
const { params, values, dataSources } = this.state;
return <>
<ParameterControls params={params} values={values} onChangeValues={values => this.setState({ values })} />
<PlotWrapper plugin={this.plugin} values={values} dataSources={dataSources} interactivity={this.interactivity} />
</>;
}
}
export function MAPairwiseScorePlot({ plugin, model, pairwiseMetric }: { plugin: PluginContext, model: Model, pairwiseMetric: QualityAssessment.Pairwise }) {
const _interactivity = useRef<BehaviorSubject<PlotInteractivityState>>();
const interactivity = _interactivity.current ??= new BehaviorSubject<PlotInteractivityState>({});
useEffect(() => {
const queue = new SingleAsyncQueue();
const highlight = filterHighlightState(interactivity).subscribe(state => highlightState(plugin, state));
const paint = filterOverpaintState(interactivity).subscribe(state => queue.enqueue(() => overpaintState(plugin, state)));
return () => {
highlight.unsubscribe();
paint.unsubscribe();
queue.enqueue(() => overpaintState(plugin, interactivity.value));
};
}, [model, pairwiseMetric]);
return <MAPairwiseScorePlotBase model={model} pairwiseMetric={pairwiseMetric} interactivity={interactivity} />;
}
function filterHighlightState(state: BehaviorSubject<PlotInteractivityState>) {
return state.pipe(
throttleTime(16, undefined, { leading: true, trailing: true }),
distinctUntilChanged((a, b) => a.crosshairOffset === b.crosshairOffset)
);
}
function filterOverpaintState(state: BehaviorSubject<PlotInteractivityState>) {
return state.pipe(
throttleTime(66, undefined, { leading: true, trailing: true }),
distinctUntilChanged((a, b) => a.boxStart === b.boxStart && (a.mouseDown ? a.crosshairOffset : a.boxEnd) === (b.mouseDown ? b.crosshairOffset : b.boxEnd))
);
}
const PlotWrapper = memo(({ plugin, values, dataSources, interactivity }: { plugin: PluginContext, values: State['values'], dataSources: State['dataSources'], interactivity: BehaviorSubject<PlotInteractivityState> }) => {
const model: Model | undefined = plugin.managers.structure.hierarchy.current.models.find(m => m.cell.transform.ref === values.model)?.cell.obj?.data;
const src = dataSources.find(src => src.id === values.data);
const cif: PluginStateObject.Format.Cif | undefined = plugin.state.data.cells.get(src?.dataRef!)?.obj;
const block = cif?.data.blocks[src?.blockIndex!];
if (!model || !block || !src) return <div className='msp-description'>Data not available</div>;
const metric = QualityAssessment.pairwiseMetricFromModelArchiveCIF(model, block, src.metridId);
if (!metric) return <div className='msp-description'>Data not available</div>;
return <MAPairwiseScorePlotBase interactivity={interactivity} model={model} pairwiseMetric={metric} />;
}, (prev, next) => prev.values.data === next.values.data && prev.values.model === next.values.model);
function getPropsAndValues(plugin: PluginContext, current?: { model?: string, data?: string }) {
const models = plugin.managers.structure.hierarchy.current.models;
const cifs = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Format.Cif));
const dataSources: {
id: string,
label: string,
metridId: number,
dataRef: StateTransform.Ref,
blockIndex: number,
}[] = [];
for (const cif of cifs) {
if (!cif.obj?.data.blocks) continue;
let blockIndex = 0;
for (const block of cif.obj.data.blocks) {
for (const pae of QualityAssessment.findModelArchiveCIFPAEMetrics(block)) {
dataSources.push({
id: `${cif.transform.ref}:${blockIndex}:${pae.id}`,
metridId: pae.id,
label: `${block.header}: ${pae.name}`,
dataRef: cif.transform.ref,
blockIndex,
});
}
blockIndex++;
}
}
const params = {
model: PD.Select(models[0]?.cell.transform.ref, models.map(m => [m.cell.transform.ref, m.cell.obj?.data.label!]), { isHidden: models.length <= 1 }),
data: PD.Select(dataSources[0]?.id, dataSources.map(o => [o.id, o.label]), { isHidden: dataSources.length <= 1 })
};
const values = {
model: params.model.options.find(o => o[0] === current?.model)?.[0] ?? params.model.options[0]?.[0],
data: params.data.options.find(o => o[0] === current?.data)?.[0] ?? params.data.options[0]?.[0],
};
return { params, values, dataSources };
}
const PlotSize = 1000;
const PlotOffset = 120;
const PlotColors = {
ScoredOverpaint: Color(0xFFA500),
ScoredLabel: Color(0xBC7100),
AlignedOverpaint: Color(0x1AFFBB),
AlignedLabel: Color(0x0F8E68),
};
interface PlotInteractivityState {
model?: Model;
drawing?: MAPairwiseMetricDrawing;
crosshairOffset?: [number, number];
inside?: boolean;
mouseDown?: boolean;
boxStart?: [number, number];
boxEnd?: [number, number];
}
export const MAPairwiseScorePlotBase = memo(({ model, pairwiseMetric, interactivity }: { model: Model, pairwiseMetric: QualityAssessment.Pairwise, interactivity: BehaviorSubject<PlotInteractivityState> }) => {
const interactivityRect = useRef<SVGRectElement>();
const drawing = maDrawPairwiseMetricPNG(model, pairwiseMetric);
useEffect(() => {
if (!drawing) {
interactivity.next({});
return;
}
interactivity.next({ model, drawing });
const moveEvent = (ev: MouseEvent) => {
const current = interactivity.value;
if (!current.inside && !current.mouseDown) return;
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
interactivity.next({ ...current, crosshairOffset: offset });
};
const mouseUpEvent = (ev: MouseEvent) => {
if (!interactivity.value.mouseDown) return;
const offset = getPlotMouseOffsetBase(interactivityRect.current!, ev.clientX, ev.clientY);
interactivity.next({ ...interactivity.value, mouseDown: false, boxEnd: offset });
};
window.addEventListener('mousemove', moveEvent);
window.addEventListener('mouseup', mouseUpEvent);
return () => {
window.removeEventListener('mousemove', moveEvent);
window.removeEventListener('mouseup', mouseUpEvent);
};
}, [model, interactivity, drawing]);
if (!drawing) return <>Not available</>;
const { metric, colorRange, chains, png } = drawing;
const nResidues = metric.residueRange[1] - metric.residueRange[0];
const border = '#333';
const line = '#000';
const legendHeight = 80;
const legendOffsetY = PlotOffset + PlotSize + 50;
const viewBox = '0 0 1140 1270';
return <div style={{ margin: '8px 8px 0 8px', position: 'relative' }}>
<svg viewBox={viewBox} width='100%'>
<image x={PlotOffset + 1} y={PlotOffset + 1} width={PlotSize - 1} height={PlotSize - 1} href={png} />
<line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ stroke: line, strokeDasharray: '15,15' }} />
<linearGradient id='legend-gradient' x1={0} x2={1} y1={0} y2={0}>
<stop offset='0%' stopColor={colorRange[0]} />
<stop offset='100%' stopColor={colorRange[1]} />
</linearGradient>
<rect x={PlotOffset} y={legendOffsetY} width={PlotSize} height={legendHeight} style={{ fill: 'url(#legend-gradient)', strokeWidth: 1, stroke: border }} />
<text x={PlotOffset + 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'white', fontWeight: 'bold' }}>{round(metric.valueRange[0], 2)} Å</text>
<text x={PlotOffset + PlotSize - 20} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black', fontWeight: 'bold' }} textAnchor='end'>{round(metric.valueRange[1], 2)} Å</text>
<text x={PlotOffset + PlotSize / 2} y={legendOffsetY + legendHeight - 22} style={{ fontSize: '45px', fill: 'black' }} textAnchor='middle'>Predicted Aligned Error</text>
<text x={PlotOffset + PlotSize / 2} y={50} style={{ fontSize: '45px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.ScoredLabel) }} textAnchor='middle'>Scored Residue</text>
<text className='msp-svg-text' style={{ fontSize: '50px', fontWeight: 'bold', fill: Color.toStyle(PlotColors.AlignedLabel) }} transform={`translate(50, ${PlotOffset + PlotSize / 2}) rotate(270)`} textAnchor='middle'>Aligned Residue</text>
{chains.map(({ startOffset, endOffset, label }) => {
const textOffset = PlotOffset + PlotSize * (startOffset + (endOffset - startOffset) / 2) / nResidues;
const endLineOffset = PlotOffset + PlotSize * endOffset / nResidues;
const startLineOffset = PlotOffset + PlotSize * startOffset / nResidues;
const seq_id = model.atomicHierarchy.residues.label_seq_id;
const startIndex = seq_id.value(metric.residueRange[0] + startOffset);
const endIndex = seq_id.value(metric.residueRange[0] + endOffset - 1);
return <Fragment key={startOffset}>
<text x={textOffset} y={PlotOffset - 15} className='msp-svg-text' style={{ fontSize: '40px' }} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
<text className='msp-svg-text' style={{ fontSize: '40px' }} transform={`translate(${PlotOffset - 15}, ${textOffset}) rotate(270)`} textAnchor='middle'>{label} {startIndex}-{endIndex}</text>
<line x1={startLineOffset} x2={startLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
<line x1={endLineOffset} x2={endLineOffset} y1={PlotOffset - 20} y2={PlotOffset + PlotSize + 20} style={{ stroke: line, strokeDasharray: '15,15' }} />
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={startLineOffset} y2={startLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
<line x1={PlotOffset - 20} x2={PlotOffset + PlotSize + 20} y1={endLineOffset} y2={endLineOffset} style={{ stroke: line, strokeDasharray: '15,15' }} />
</Fragment>;
})}
</svg>
<svg viewBox={viewBox} style={{ position: 'absolute', inset: 0 }}>
<rect x={PlotOffset} y={PlotOffset} width={PlotSize} height={PlotSize} style={{ fill: 'transparent', cursor: 'crosshair' }}
ref={interactivityRect as any}
onMouseMove={(ev) => {
interactivity.next({ ...interactivity.value, inside: true });
ev.currentTarget.style.stroke = 'black';
ev.currentTarget.style.strokeWidth = '4px';
}}
onMouseDown={(ev) => {
interactivity.next({ ...interactivity.value, mouseDown: true, boxStart: getPlotMouseOffset(ev) });
}}
onMouseLeave={(ev) => {
interactivity.next({ ...interactivity.value, inside: false, crosshairOffset: undefined });
ev.currentTarget.style.stroke = '#333';
ev.currentTarget.style.strokeWidth = '1px';
}} />
<PlotInteractivity drawing={drawing} interactity={interactivity} />
</svg>
</div>;
}, (prev, next) => prev.model === next.model && prev.pairwiseMetric === next.pairwiseMetric);
function PlotInteractivity({ drawing, interactity }: { drawing: MAPairwiseMetricDrawing, interactity: BehaviorSubject<PlotInteractivityState> }) {
const state = useBehavior(interactity);
const { crosshairOffset, inside } = state;
const box = getBox(state);
const label = getCrosshairLabel(state);
let labelNode: ReactNode | undefined;
if (label) {
const labelStyle: CSSProperties | undefined = label ? { fontSize: '45px', fill: 'black', fontWeight: 'bold', pointerEvents: 'none', userSelect: 'none' } : undefined;
let x: number, y: number, anchor: string;
if (crosshairOffset![0] < PlotSize / 2) {
x = PlotOffset + crosshairOffset![0] + 20;
anchor = 'start';
} else {
x = PlotOffset + crosshairOffset![0] - 20;
anchor = 'end';
}
if (crosshairOffset![1] < PlotSize / 2) {
y = PlotOffset + crosshairOffset![1] + 65;
} else {
y = PlotOffset + crosshairOffset![1] - (label[2] ? 3 * 45 : 2 * 45) + 20;
}
labelNode = <text y={y} style={labelStyle} textAnchor={anchor}>
<tspan x={x}>S: {label[0]}</tspan>
<tspan x={x} dy={45}>A: {label[1]}</tspan>
{label[2] && <tspan x={x} dy={45}>{label[2]}</tspan>}
</text>;
}
return <>
{inside && crosshairOffset && <line x1={crosshairOffset[0] + PlotOffset} x2={crosshairOffset[0] + PlotOffset} y1={PlotOffset} y2={PlotOffset + PlotSize} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
{inside && crosshairOffset && <line x1={PlotOffset} x2={PlotOffset + PlotSize} y1={crosshairOffset[1] + PlotOffset} y2={crosshairOffset[1] + PlotOffset} style={{ pointerEvents: 'none', stroke: 'black', strokeDasharray: '5,5' }} />}
{box && <rect x={PlotOffset + box[0]} y={PlotOffset + box[1]} width={box[2]} height={box[3]} style={{ stroke: '#eee', strokeWidth: 4, fill: 'rgba(0, 0, 0, 0.15)', pointerEvents: 'none' }} />}
{labelNode}
</>;
}
function getCrosshairLabel(state: PlotInteractivityState) {
if (!state.drawing || !state.crosshairOffset || !state.inside) return;
const { drawing } = state;
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
const value = drawing.metric.values[rA]?.[rB] ?? drawing.metric.values[rB]?.[rA];
const valueLabel = typeof value === 'number' ? `${round(value, 2)} Å` : '';
return [getResidueLabel(drawing, rA), getResidueLabel(drawing, rB), valueLabel];
}
function getResidueIndex(drawing: MAPairwiseMetricDrawing, offset: number) {
const rI = drawing.metric.residueRange[0] + Math.round(offset / PlotSize * (drawing.metric.residueRange[1] - drawing.metric.residueRange[0] + 1)) as ResidueIndex;
return clamp(rI, drawing.metric.residueRange[0], drawing.metric.residueRange[1]) as ResidueIndex;
}
function getResidueLabel(drawing: MAPairwiseMetricDrawing, rI: ResidueIndex) {
const hierarchy = drawing.model.atomicHierarchy;
const asym_id = hierarchy.chains.label_asym_id;
const seq_id = hierarchy.residues.label_seq_id;
const comp_id = hierarchy.atoms.label_comp_id;
return `${asym_id.value(AtomicHierarchy.residueChainIndex(hierarchy, rI))} ${seq_id.value(rI)} ${comp_id.value(AtomicHierarchy.residueFirstAtomIndex(hierarchy, rI))}`;
}
function getBox(state: PlotInteractivityState) {
const start = state.boxStart;
const end = state.mouseDown ? state.crosshairOffset : state.boxEnd;
if (!start || !end) return undefined;
const x = clamp(Math.min(start[0], end[0]), 0, PlotSize);
const width = clamp(Math.max(start[0], end[0]), 0, PlotSize) - x;
const y = clamp(Math.min(start[1], end[1]), 0, PlotSize);
const height = clamp(Math.max(start[1], end[1]), 0, PlotSize) - y;
if (width < 1 && height < 1) return undefined;
return [x, y, width, height];
}
function getPlotMouseOffset(ev: React.MouseEvent<SVGRectElement, MouseEvent>) {
return getPlotMouseOffsetBase(ev.currentTarget, ev.clientX, ev.clientY);
}
function getPlotMouseOffsetBase(target: HTMLElement | SVGRectElement, clientX: number, clientY: number) {
const rect = target.getBoundingClientRect();
const offsetX = PlotSize * (clientX - rect.left) / rect.width;
const offsetY = PlotSize * (clientY - rect.top) / rect.height;
return [offsetX, offsetY] as [number, number];
}
function findModelRef(plugin: PluginContext, model: Model | undefined) {
if (!model) return undefined;
for (const m of plugin.managers.structure.hierarchy.current.models) {
if (m.cell.obj?.data === model) return m;
}
return undefined;
}
function highlightState(plugin: PluginContext, state: PlotInteractivityState) {
const structure = findModelRef(plugin, state.model)?.structures[0]?.cell.obj?.data;
if (!state.drawing || !state.crosshairOffset || !state.inside || !structure) {
plugin.managers.interactivity.lociHighlights.clearHighlights();
return;
}
const { drawing } = state;
const rA = getResidueIndex(drawing, clamp(state.crosshairOffset[0], 0, PlotSize));
const rB = getResidueIndex(drawing, clamp(state.crosshairOffset[1], 0, PlotSize));
const resIdx = StructureProperties.residue.key;
const loci = StructureQuery.loci(atoms({
residueTest: ctx => {
const rI = resIdx(ctx.element);
return rI === rA || rI === rB;
},
}), structure);
plugin.managers.interactivity.lociHighlights.highlightOnly({ loci });
}
async function overpaintState(plugin: PluginContext, state: PlotInteractivityState) {
const tag = 'modelarchive-pae-overpaint';
const overpaints = plugin.state.data.selectQ(q => q.root.subtree().withTag(tag));
const update = plugin.build();
for (const overpaint of overpaints) update.delete(overpaint);
const model = findModelRef(plugin, state.model);
const structure = model?.structures[0]?.cell.obj?.data;
if (!state.drawing || !state.boxStart || !(state.boxEnd || state.crosshairOffset) || !structure) {
if (!overpaints) return;
return reApplyRepresentationStates(plugin, update);
}
const start = state.boxStart;
const end = state.mouseDown ? state.crosshairOffset! : state.boxEnd!;
const x0 = clamp(Math.min(start[0], end[0]), 0, PlotSize);
const x1 = clamp(Math.max(start[0], end[0]), 0, PlotSize);
const y0 = clamp(Math.min(start[1], end[1]), 0, PlotSize);
const y1 = clamp(Math.max(start[1], end[1]), 0, PlotSize);
if (x1 - x0 <= 1 || y1 - y0 <= 1) {
if (!overpaints) return;
return reApplyRepresentationStates(plugin, update);
}
const representations = plugin.state.data.selectQ(q =>
q.byRef(model.cell.transform.ref!)
.subtree()
.ofType(PluginStateObject.Molecule.Structure.Representation3D)
);
const resIdx = StructureProperties.residue.key;
const startScored = getResidueIndex(state.drawing, x0);
const endScored = getResidueIndex(state.drawing, x1);
const lociScored = StructureQuery.loci(atoms({
residueTest: ctx => {
const rI = resIdx(ctx.element);
return rI >= startScored && rI <= endScored;
},
}), structure);
const startAligned = getResidueIndex(state.drawing, y0);
const endAligned = getResidueIndex(state.drawing, y1);
const lociAligned = StructureQuery.loci(atoms({
residueTest: ctx => {
const rI = resIdx(ctx.element);
return rI >= startAligned && rI <= endAligned;
},
}), structure);
const layers = [{
bundle: StructureElement.Bundle.fromSubStructure(structure, structure),
color: Color(0x777777),
clear: false,
}, {
bundle: StructureElement.Bundle.fromLoci(lociScored),
color: PlotColors.ScoredOverpaint,
clear: false,
}, {
bundle: StructureElement.Bundle.fromLoci(lociAligned),
color: PlotColors.AlignedOverpaint,
clear: false,
}];
for (const repr of representations) {
update.to(repr).apply(OverpaintStructureRepresentation3DFromBundle, { layers }, { tags: [tag], state: { isGhost: true } });
}
return update.commit();
}
async function reApplyRepresentationStates(plugin: PluginContext, update: StateBuilder.Root) {
await update.commit();
const states = plugin.state.data.selectQ(q => q.root.subtree().ofType(PluginStateObject.Molecule.Structure.Representation3DState));
for (const state of states) {
const data = state.obj?.data;
if (!data) continue;
data.repr.setState(data.state);
plugin.canvas3d?.update(data.repr);
}
}

View File

@@ -1,21 +1,24 @@
/**
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2021-24 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { Unit } from '../../../mol-model/structure';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CifFrame } from '../../../mol-io/reader/cif';
import { toDatabase } from '../../../mol-io/reader/cif/schema';
import { mmCIF_Schema } from '../../../mol-io/reader/cif/schema/mmcif';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { CustomProperty } from '../../../mol-model-props/common/custom-property';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { Unit } from '../../../mol-model/structure';
import { Model, ResidueIndex } from '../../../mol-model/structure/model';
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
import { CustomPropSymbol } from '../../../mol-script/language/symbol';
import { Type } from '../../../mol-script/language/type';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { AtomicIndex } from '../../../mol-model/structure/model/properties/atomic';
import { QuerySymbolRuntime } from '../../../mol-script/runtime/query/compiler';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export { QualityAssessment };
@@ -26,10 +29,19 @@ interface QualityAssessment {
}
namespace QualityAssessment {
export interface Pairwise {
id: number
name: string
residueRange: [ResidueIndex, ResidueIndex]
valueRange: [number, number]
values: Record<ResidueIndex, Record<ResidueIndex, number | undefined> | undefined>
}
const Empty = {
value: {
localMetrics: new Map()
}
localMetrics: new Map(),
} satisfies QualityAssessment
};
export function isApplicable(model?: Model, localMetricName?: 'pLDDT' | 'qmean'): boolean {
@@ -106,6 +118,101 @@ namespace QualityAssessment {
};
}
const PairwiseSchema = {
ma_qa_metric: mmCIF_Schema.ma_qa_metric,
ma_qa_metric_local_pairwise: mmCIF_Schema.ma_qa_metric_local_pairwise
};
export function findModelArchiveCIFPAEMetrics(frame: CifFrame) {
const { ma_qa_metric, ma_qa_metric_local_pairwise } = toDatabase(PairwiseSchema, frame);
const result: { id: number, name: string }[] = [];
if (ma_qa_metric_local_pairwise._rowCount === 0) return result;
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
const id = ma_qa_metric.id.value(i);
const name = ma_qa_metric.name.value(i);
if (!name.toLowerCase().includes('pae')) continue;
result.push({ id, name });
}
return result;
}
export function pairwiseMetricFromModelArchiveCIF(model: Model, frame: CifFrame, metricId: number): Pairwise | undefined {
const db = toDatabase(PairwiseSchema, frame);
if (!db.ma_qa_metric_local_pairwise._rowCount) return undefined;
const { ma_qa_metric, ma_qa_metric_local_pairwise } = db;
const { model_id, label_asym_id_1, label_seq_id_1, label_asym_id_2, label_seq_id_2, metric_id, metric_value } = db.ma_qa_metric_local_pairwise;
const { index } = model.atomicHierarchy;
let metric: Pairwise | undefined;
for (let i = 0, il = ma_qa_metric._rowCount; i < il; i++) {
if (ma_qa_metric.mode.value(i) !== 'local-pairwise') continue;
const id = ma_qa_metric.id.value(i);
if (id !== metricId) continue;
const name = ma_qa_metric.name.value(i);
metric = {
id,
name,
residueRange: [Number.MAX_SAFE_INTEGER as ResidueIndex, Number.MIN_SAFE_INTEGER as ResidueIndex],
valueRange: [Number.MAX_VALUE, -Number.MAX_VALUE],
values: {}
};
}
if (!metric) return undefined;
const { values, residueRange, valueRange } = metric;
const residueKey: AtomicIndex.ResidueLabelKey = {
label_entity_id: '',
label_asym_id: '',
label_seq_id: 0,
pdbx_PDB_ins_code: undefined,
};
for (let i = 0, il = ma_qa_metric_local_pairwise._rowCount; i < il; i++) {
if (model_id.value(i) !== model.modelNum || metric_id.value(i) !== metricId) continue;
let labelAsymId = label_asym_id_1.value(i);
let entityIndex = index.findEntity(labelAsymId);
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
residueKey.label_asym_id = labelAsymId;
residueKey.label_seq_id = label_seq_id_1.value(i);
const rI_1 = index.findResidueLabel(residueKey);
if (rI_1 < 0) continue;
labelAsymId = label_asym_id_2.value(i);
entityIndex = index.findEntity(labelAsymId);
residueKey.label_entity_id = model.entities.data.id.value(entityIndex);
residueKey.label_asym_id = labelAsymId;
residueKey.label_seq_id = label_seq_id_2.value(i);
const rI_2 = index.findResidueLabel(residueKey);
if (rI_1 < 0) continue;
let r1 = values[rI_1];
if (!r1) {
r1 = {};
values[rI_1] = r1;
}
const value = metric_value.value(i);
r1[rI_2] = value;
if (rI_1 < residueRange[0]) residueRange[0] = rI_1;
if (rI_2 < residueRange[0]) residueRange[0] = rI_2;
if (rI_1 > residueRange[1]) residueRange[1] = rI_1;
if (rI_2 > residueRange[1]) residueRange[1] = rI_2;
if (value < valueRange[0]) valueRange[0] = value;
if (value > valueRange[1]) valueRange[1] = value;
}
return metric;
}
export const symbols = {
pLDDT: QuerySymbolRuntime.Dynamic(CustomPropSymbol('ma', 'quality-assessment.pLDDT', Type.Num),
ctx => {

View File

@@ -1,33 +1,29 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { Camera } from '../../mol-canvas3d/camera';
import { Canvas3DParams } from '../../mol-canvas3d/canvas3d';
import { GraphicsRenderObject } from '../../mol-gl/render-object';
import { Sphere3D } from '../../mol-math/geometry';
import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
import { Vec3 } from '../../mol-math/linear-algebra';
import { Loci } from '../../mol-model/loci';
import { Structure } from '../../mol-model/structure';
import { PluginStateObject } from '../../mol-plugin-state/objects';
import { getFocusSnapshot, getPluginBoundingSphere } from '../../mol-plugin-state/manager/focus-camera/focus-object';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { PluginState } from '../../mol-plugin/state';
import { StateObjectSelector } from '../../mol-state';
import { ColorNames } from '../../mol-util/color/names';
import { decodeColor } from './helpers/utils';
import { ParamsOfKind } from './tree/generic/tree-schema';
import { MolstarTree } from './tree/molstar/molstar-tree';
import { MVSDefaults } from './tree/mvs/mvs-defaults';
import { MolstarLoadingContext } from './load';
import { SnapshotMetadata } from './mvs-data';
import { MolstarNodeParams } from './tree/molstar/molstar-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
const DefaultFocusOptions = {
minRadius: 5,
extraRadiusForFocus: 0,
extraRadiusForZoomAll: 0,
extraRadius: 0,
};
const DefaultCanvasBackgroundColor = ColorNames.white;
@@ -42,45 +38,49 @@ export async function suppressCameraAutoreset(plugin: PluginContext) {
}
/** Set the camera based on a camera node params. */
export async function setCamera(plugin: PluginContext, params: ParamsOfKind<MolstarTree, 'camera'>) {
const target = Vec3.create(...params.target);
let position = Vec3.create(...params.position);
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
const up = Vec3.create(...params.up);
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius: Infinity }; // `radius: Infinity` avoids clipping (ensures covering the whole scene)
export async function setCamera(plugin: PluginContext, params: MolstarNodeParams<'camera'>) {
const snapshot = cameraParamsToCameraSnapshot(plugin, params);
adjustSceneRadiusFactor(plugin, snapshot.target);
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
}
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is null).
* Orient the camera based on a focus node params. */
export async function setFocus(plugin: PluginContext, structureNodeSelector: StateObjectSelector | undefined, params: ParamsOfKind<MolstarTree, 'focus'> = MVSDefaults.focus) {
let structure: Structure | undefined = undefined;
if (structureNodeSelector) {
const cell = plugin.state.data.cells.get(structureNodeSelector.ref);
structure = cell?.obj?.data;
if (!structure) console.warn('Focus: no structure');
if (!(structure instanceof Structure)) {
console.warn('Focus: cannot apply to a non-structure node');
structure = undefined;
}
}
const boundingSphere = structure ? Loci.getBoundingSphere(Structure.Loci(structure)) : getPluginBoundingSphere(plugin);
if (boundingSphere && plugin.canvas3d) {
const extraRadius = structure ? DefaultFocusOptions.extraRadiusForFocus : DefaultFocusOptions.extraRadiusForZoomAll;
const direction = Vec3.create(...params.direction);
const up = Vec3.create(...params.up);
Vec3.orthogonalize(up, direction, up);
const snapshot = snapshotFromSphereAndDirections(plugin.canvas3d.camera, {
center: boundingSphere.center,
radius: boundingSphere.radius + extraRadius,
up,
direction,
});
resetSceneRadiusFactor(plugin);
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
}
export function cameraParamsToCameraSnapshot(plugin: PluginContext, params: MolstarNodeParams<'camera'>): Partial<Camera.Snapshot> {
const target = Vec3.create(...params.target);
let position = Vec3.create(...params.position);
const radius = Vec3.distance(target, position) / 2;
if (plugin.canvas3d) position = fovAdjustedPosition(target, position, plugin.canvas3d.camera.state.mode, plugin.canvas3d.camera.state.fov);
const up = Vec3.create(...params.up);
Vec3.orthogonalize(up, Vec3.sub(_tmpVec, target, position), up);
const snapshot: Partial<Camera.Snapshot> = { target, position, up, radius, radiusMax: radius };
return snapshot;
}
/** Focus the camera on the bounding sphere of a (sub)structure (or on the whole scene if `structureNodeSelector` is undefined).
* Orient the camera based on a focus node params. **/
export async function setFocus(plugin: PluginContext, focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]) {
const snapshot = getFocusSnapshot(plugin, {
...snapshotFocusInfoFromMvsFocuses(focuses),
minRadius: DefaultFocusOptions.minRadius,
});
if (!snapshot) return;
resetSceneRadiusFactor(plugin);
await PluginCommands.Camera.SetSnapshot(plugin, { snapshot });
}
function snapshotFocusInfoFromMvsFocuses(focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[]): PluginState.SnapshotFocusInfo {
const lastFocus = (focuses.length > 0) ? focuses[focuses.length - 1] : undefined;
const direction = lastFocus?.params.direction ?? MVSTreeSchema.nodes.focus.params.fields.direction.default;
const up = lastFocus?.params.up ?? MVSTreeSchema.nodes.focus.params.fields.up.default;
return {
targets: focuses.map<PluginState.SnapshotFocusTargetInfo>(f => ({
targetRef: f.target.ref === '-=root=-' ? undefined : f.target.ref, // need to treat root separately so it does not include invisible structure parts etc.
radius: f.params.radius ?? undefined,
radiusFactor: f.params.radius_factor,
extraRadius: f.params.radius_extent,
})),
direction: Vec3.create(...direction),
up: Vec3.create(...up),
};
}
/** Adjust `sceneRadiusFactor` property so that the current scene is not cropped */
@@ -98,19 +98,6 @@ function resetSceneRadiusFactor(plugin: PluginContext) {
plugin.canvas3d?.setProps({ sceneRadiusFactor });
}
/** Return camera snapshot for focusing a sphere with given `center` and `radius`,
* while ensuring given view `direction` (aligns with vector position->target)
* and `up` (aligns with screen Y axis). */
function snapshotFromSphereAndDirections(camera: Camera, options: { center: Vec3, radius: number, direction: Vec3, up: Vec3 }): Partial<Camera.Snapshot> {
// This might seem to repeat `plugin.canvas3d.camera.getFocus` but avoid flipping
const { center, direction, up } = options;
const radius = Math.max(options.radius, DefaultFocusOptions.minRadius);
const distance = camera.getTargetDistance(radius);
const deltaDirection = Vec3.setMagnitude(_tmpVec, direction, distance);
const position = Vec3.sub(Vec3(), center, deltaDirection);
return { target: center, position, up, radius };
}
/** Return the distance adjustment ratio for conversion from the "reference camera"
* to a camera with an arbitrary field of view `fov`. */
function distanceAdjustment(mode: Camera.Mode, fov: number) {
@@ -129,41 +116,35 @@ function fovAdjustedPosition(target: Vec3, refPosition: Vec3, mode: Camera.Mode,
return Vec3.scaleAndAdd(delta, target, delta, adjustment); // return target + delta * adjustment
}
/** Compute the bounding sphere of the whole scene. */
function getPluginBoundingSphere(plugin: PluginContext) {
const renderObjects = getRenderObjects(plugin, false);
const spheres = renderObjects.map(r => r.values.boundingSphere.ref.value).filter(sphere => sphere.radius > 0);
return boundingSphereOfSpheres(spheres);
}
function getRenderObjects(plugin: PluginContext, includeHidden: boolean): GraphicsRenderObject[] {
let reprCells = Array.from(plugin.state.data.cells.values()).filter(cell => cell.obj && PluginStateObject.isRepresentation3D(cell.obj));
if (!includeHidden) reprCells = reprCells.filter(cell => !cell.state.isHidden);
const renderables = reprCells.flatMap(cell => cell.obj!.data.repr.renderObjects);
return renderables;
}
let boundaryHelper: BoundaryHelper | undefined = undefined;
function boundingSphereOfSpheres(spheres: Sphere3D[]): Sphere3D {
boundaryHelper ??= new BoundaryHelper('98');
boundaryHelper.reset();
for (const s of spheres) boundaryHelper.includeSphere(s);
boundaryHelper.finishedIncludeStep();
for (const s of spheres) boundaryHelper.radiusSphere(s);
return boundaryHelper.getSphere();
/** Create object for PluginState.Snapshot.camera based on tree loading context and MVS snapshot metadata */
export function createPluginStateSnapshotCamera(plugin: PluginContext, context: MolstarLoadingContext, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }): PluginState.Snapshot['camera'] {
const camera: PluginState.Snapshot['camera'] = {
transitionStyle: 'animate',
transitionDurationInMs: metadata.previousTransitionDurationMs ?? 0,
};
if (context.camera.cameraParams !== undefined) {
const currentCameraSnapshot = plugin.canvas3d!.camera.getSnapshot();
const cameraSnapshot = cameraParamsToCameraSnapshot(plugin, context.camera.cameraParams);
camera.current = { ...currentCameraSnapshot, ...cameraSnapshot };
} else {
camera.focus = snapshotFocusInfoFromMvsFocuses(context.camera.focuses);
}
return camera;
}
/** Set canvas properties based on a canvas node params. */
export function setCanvas(plugin: PluginContext, params: ParamsOfKind<MolstarTree, 'canvas'> | undefined) {
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
if (backgroundColor !== plugin.canvas3d?.props.renderer.backgroundColor) {
plugin.canvas3d?.setProps(old => ({
...old,
renderer: {
...old.renderer,
backgroundColor: backgroundColor,
}
}));
}
export function setCanvas(plugin: PluginContext, params: MolstarNodeParams<'canvas'> | undefined) {
plugin.canvas3d?.setProps(old => modifyCanvasProps(old, params));
}
/** Create a deep copy of `oldCanvasProps` with values modified according to a canvas node params. */
export function modifyCanvasProps(oldCanvasProps: Canvas3DProps, params: MolstarNodeParams<'canvas'> | undefined): Canvas3DProps {
const backgroundColor = decodeColor(params?.background_color) ?? DefaultCanvasBackgroundColor;
return {
...oldCanvasProps,
renderer: {
...oldCanvasProps.renderer,
backgroundColor: backgroundColor,
},
};
}

View File

@@ -59,6 +59,7 @@ export const ParseMVSX = MVSTransform({
export const LoadMvsDataParams = {
replaceExisting: PD.Boolean(false, { description: 'If true, the loaded MVS view will replace the current state; if false, the MVS view will be added to the current state.' }),
keepCamera: PD.Boolean(false, { description: 'If true, any camera positioning from the MVS state will be ignored and the current camera position will be kept.' }),
applyExtensions: PD.Boolean(true, { description: 'If true, apply builtin MVS-loading extensions (not a part of standard MVS specification).' }),
};
/** State action which loads a MVS view into Mol* */
@@ -68,7 +69,7 @@ export const LoadMvsData = StateAction.build({
params: LoadMvsDataParams,
})(({ a, params }, plugin: PluginContext) => Task.create('Load MVS Data', async () => {
const { mvsData, sourceUrl } = a.data;
await loadMVS(plugin, mvsData, { replaceExisting: params.replaceExisting, keepCamera: params.keepCamera, sourceUrl: sourceUrl });
await loadMVS(plugin, mvsData, { replaceExisting: params.replaceExisting, keepCamera: params.keepCamera, sourceUrl: sourceUrl, extensions: params.applyExtensions ? undefined : [] });
}));

View File

@@ -0,0 +1,662 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { Lines } from '../../../mol-geo/geometry/lines/lines';
import { LinesBuilder } from '../../../mol-geo/geometry/lines/lines-builder';
import { addFixedCountDashedCylinder, addSimpleCylinder, BasicCylinderProps } from '../../../mol-geo/geometry/mesh/builder/cylinder';
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
import { Text } from '../../../mol-geo/geometry/text/text';
import { TextBuilder } from '../../../mol-geo/geometry/text/text-builder';
import { Box3D, Sphere3D } from '../../../mol-math/geometry';
import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
import { Shape } from '../../../mol-model/shape';
import { Structure, StructureElement, StructureSelection } from '../../../mol-model/structure';
import { StructureQueryHelper } from '../../../mol-plugin-state/helpers/structure-query';
import { PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { PluginContext } from '../../../mol-plugin/context';
import { Expression } from '../../../mol-script/language/expression';
import { StateObject } from '../../../mol-state';
import { Task } from '../../../mol-task';
import { round } from '../../../mol-util';
import { range } from '../../../mol-util/array';
import { Asset } from '../../../mol-util/assets';
import { Color } from '../../../mol-util/color';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
import { capitalize } from '../../../mol-util/string';
import { rowsToExpression, rowToExpression } from '../helpers/selections';
import { collectMVSReferences, decodeColor } from '../helpers/utils';
import { MolstarNode, MolstarSubtree } from '../tree/molstar/molstar-tree';
import { MVSNode } from '../tree/mvs/mvs-tree';
import { isComponentExpression, isPrimitiveComponentExpressions, isVector3, PrimitivePositionT } from '../tree/mvs/param-types';
import { MVSTransform } from './annotation-structure-component';
type PrimitivesParams = MolstarNode<'primitives'>['params']
type _PrimitiveParams = MolstarNode<'primitive'>['params']
type PrimitiveKind = _PrimitiveParams['kind']
type PrimitiveParams<T extends PrimitiveKind = PrimitiveKind> = Extract<_PrimitiveParams, { kind: T }>
export function getPrimitiveStructureRefs(primitives: MolstarSubtree<'primitives'>) {
const refs = new Set<string>();
for (const c of primitives.children ?? []) {
if (c.kind !== 'primitive') continue;
const p = c.params;
Builders[p.kind].resolveRefs?.(p, refs);
}
return refs;
}
export class MVSPrimitivesData extends SO.Create<PrimitiveBuilderContext>({ name: 'Primitive Data', typeClass: 'Object' }) { }
export class MVSPrimitiveShapes extends SO.Create<{ mesh?: Shape<Mesh>, labels?: Shape<Text> }>({ name: 'Primitive Shapes', typeClass: 'Object' }) { }
export type MVSDownloadPrimitiveData = typeof MVSDownloadPrimitiveData
export const MVSDownloadPrimitiveData = MVSTransform({
name: 'mvs-download-primitive-data',
display: { name: 'MVS Primitives' },
from: [SO.Root, SO.Molecule.Structure],
to: MVSPrimitivesData,
params: {
uri: PD.Url('', { isHidden: true }),
format: PD.Text<'mvs-node-json'>('mvs-node-json', { isHidden: true })
},
})({
apply({ a, params, cache }, plugin: PluginContext) {
return Task.create('Download Primitive Data', async ctx => {
const url = Asset.getUrlAsset(plugin.managers.asset, params.uri);
const asset = await plugin.managers.asset.resolve(url, 'string').runInContext(ctx);
const node = JSON.parse(asset.data) as MolstarSubtree<'primitives'>;
(cache as any).asset = asset;
return new MVSPrimitivesData({
node,
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
structureRefs: {},
primitives: getPrimitives(node),
options: { ...node.params },
positionCache: new Map(),
instances: getInstances(node.params),
}, { label: 'Primitive Data' });
});
},
dispose({ cache }) {
((cache as any)?.asset as Asset.Wrapper | undefined)?.dispose();
},
});
export type MVSInlinePrimitiveData = typeof MVSInlinePrimitiveData
export const MVSInlinePrimitiveData = MVSTransform({
name: 'mvs-inline-primitive-data',
display: { name: 'MVS Primitives' },
from: [SO.Root, SO.Molecule.Structure],
to: MVSPrimitivesData,
params: {
node: PD.Value<MolstarSubtree<'primitives'>>(undefined as any, { isHidden: true }),
},
})({
apply({ a, params }) {
return new MVSPrimitivesData({
node: params.node,
defaultStructure: SO.Molecule.Structure.is(a) ? a.data : undefined,
structureRefs: {},
primitives: getPrimitives(params.node),
options: { ...params.node.params },
positionCache: new Map(),
instances: getInstances(params.node.params),
}, { label: 'Primitive Data' });
}
});
export type MVSBuildPrimitiveShape = typeof MVSBuildPrimitiveShape
export const MVSBuildPrimitiveShape = MVSTransform({
name: 'mvs-build-primitive-shape',
display: { name: 'MVS Primitives' },
from: MVSPrimitivesData,
to: SO.Shape.Provider,
params: {
kind: PD.Text<'mesh' | 'labels' | 'lines'>('mesh')
}
})({
apply({ a, params, dependencies }) {
const structureRefs = dependencies ? collectMVSReferences([SO.Molecule.Structure], dependencies) : {};
const context: PrimitiveBuilderContext = { ...a.data, structureRefs };
const label = capitalize(params.kind);
if (params.kind === 'mesh') {
if (!hasPrimitiveKind(a.data, 'mesh')) return StateObject.Null;
return new SO.Shape.Provider({
label,
data: context,
params: PD.withDefaults(Mesh.Params, { alpha: a.data.options?.opacity ?? 1 }),
getShape: (_, data, __, prev: any) => buildPrimitiveMesh(data, prev?.geometry),
geometryUtils: Mesh.Utils,
}, { label });
} else if (params.kind === 'labels') {
if (!hasPrimitiveKind(a.data, 'label')) return StateObject.Null;
return new SO.Shape.Provider({
label,
data: context,
params: PD.withDefaults(DefaultLabelParams, { alpha: a.data.options?.label_opacity ?? 1 }),
getShape: (_, data, __, prev: any) => buildPrimitiveLabels(data, prev?.geometry),
geometryUtils: Text.Utils,
}, { label });
} else if (params.kind === 'lines') {
if (!hasPrimitiveKind(a.data, 'line')) return StateObject.Null;
return new SO.Shape.Provider({
label,
data: context,
params: PD.withDefaults(Lines.Params, { alpha: a.data.options?.opacity ?? 1 }),
getShape: (_, data, __, prev: any) => buildPrimitiveLines(data, prev?.geometry),
geometryUtils: Lines.Utils,
}, { label });
}
return StateObject.Null;
}
});
/* **************************************************** */
class GroupManager {
private current = -1;
groupToNodeMap = new Map<number, MVSNode<'primitive'>>();
sizes = new Map<number, number>();
colors = new Map<number, number>();
tooltips = new Map<number, string>();
allocateSingle(node: MVSNode<'primitive'>) {
const group = ++this.current;
this.groupToNodeMap.set(group, node);
return group;
}
allocateMany(node: MVSNode<'primitive'>, groups: number[]) {
const newGroups = new Map<number, number>();
const base = this.current;
for (const g of groups) {
if (newGroups.has(g)) continue;
const group = base + newGroups.size + 1;
this.groupToNodeMap.set(group, node);
newGroups.set(g, group);
}
this.current += newGroups.size + 1;
return newGroups;
}
updateColor(group: number, color?: string | null) {
const c = decodeColor(color);
if (typeof c === 'number') this.colors.set(group, c);
}
updateTooltip(group: number, tooltip?: string | null) {
if (typeof tooltip === 'string') this.tooltips.set(group, tooltip);
}
updateSize(group: number, size?: number | null) {
if (typeof size === 'number') this.sizes.set(group, size);
}
}
interface PrimitiveBuilderContext {
node: MolstarNode<'primitives'>;
defaultStructure?: Structure;
structureRefs: Record<string, Structure | undefined>;
primitives: MolstarNode<'primitive'>[];
options: PrimitivesParams;
positionCache: Map<string, [Sphere3D, Box3D]>;
instances: Mat4[] | undefined;
}
interface MeshBuilderState {
groups: GroupManager;
mesh: MeshBuilder.State;
}
interface LabelBuilderState {
groups: GroupManager;
labels: TextBuilder;
}
interface LineBuilderState {
groups: GroupManager;
lines: LinesBuilder;
}
const BaseLabelProps: PD.Values<Text.Params> = {
...PD.getDefaultValues(Text.Params),
attachment: 'middle-center',
fontQuality: 3,
fontWeight: 'normal',
borderWidth: 0.15,
borderColor: Color(0x0),
background: false,
backgroundOpacity: 0.5,
tether: false,
};
const DefaultLabelParams = PD.withDefaults(Text.Params, BaseLabelProps);
interface PrimitiveBuilder {
builders: {
mesh?: (context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: any) => void,
line?: (context: PrimitiveBuilderContext, state: LineBuilderState, node: MVSNode<'primitive'>, params: any) => void,
label?: (context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: any) => void,
}
isApplicable?: {
mesh?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
line?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
label?: ((primitive: any, context: PrimitiveBuilderContext) => boolean),
},
resolveRefs?: (params: any, refs: Set<string>) => void,
}
const Builders: Record<PrimitiveParams['kind'], PrimitiveBuilder> = {
mesh: {
builders: {
mesh: addMesh,
line: addMeshWireframe,
},
isApplicable: {
mesh: (m: PrimitiveParams<'mesh'>) => m.show_triangles,
line: (m: PrimitiveParams<'mesh'>) => m.show_wireframe,
},
},
lines: {
builders: {
line: addLines,
},
},
tube: {
builders: {
mesh: addTubeMesh,
},
resolveRefs: resolveLineRefs,
},
label: {
builders: {
label: addPrimitiveLabel,
},
resolveRefs: resolveLabelRefs,
},
distance_measurement: {
builders: {
mesh: addDistanceMesh,
label: addDistanceLabel,
},
resolveRefs: resolveLineRefs,
},
};
function getPrimitives(primitives: MolstarSubtree<'primitives'>) {
return (primitives.children ?? []).filter(c => c.kind === 'primitive') as MolstarNode<'primitive'>[];
}
function addRef(position: PrimitivePositionT, refs: Set<string>) {
if (isPrimitiveComponentExpressions(position) && position.structure_ref) {
refs.add(position.structure_ref);
}
}
function hasPrimitiveKind(context: PrimitiveBuilderContext, kind: 'mesh' | 'line' | 'label') {
for (const c of context.primitives) {
const params = c.params;
const b = Builders[params.kind];
const builderFunction = b.builders[kind];
if (builderFunction) {
const test = b.isApplicable?.[kind];
if (test === undefined || test(params, context)) {
return true;
}
}
}
return false;
}
function resolveBasePosition(context: PrimitiveBuilderContext, position: PrimitivePositionT, targetPosition: Vec3) {
return resolvePosition(context, position, targetPosition, undefined, undefined);
}
const _EmptySphere = Sphere3D.zero();
const _EmptyBox = Box3D.zero();
function resolvePosition(context: PrimitiveBuilderContext, position: PrimitivePositionT, targetPosition: Vec3 | undefined, targetSphere: Sphere3D | undefined, targetBox: Box3D | undefined) {
let expr: Expression | undefined;
let pivotRef: string | undefined;
if (isVector3(position)) {
if (targetPosition) Vec3.copy(targetPosition, position as any);
if (targetSphere) Sphere3D.set(targetSphere, position as any, 0);
if (targetBox) Box3D.set(targetBox, position as any, position as any);
return;
}
if (isPrimitiveComponentExpressions(position)) {
// TODO: take schema into account for possible optimization
expr = rowsToExpression(position.expressions!);
pivotRef = position.structure_ref;
} else if (isComponentExpression(position)) {
expr = rowToExpression(position);
}
if (!expr) {
console.error('Invalid expression', position);
throw new Error('Invalid primitive potition expression, see console for details.');
}
const pivot = !pivotRef ? context.defaultStructure : context.structureRefs[pivotRef];
if (!pivot) {
throw new Error(`Structure with ref '${pivotRef ?? '<default>'}' not found.`);
}
const cackeKey = JSON.stringify(position);
if (context.positionCache.has(cackeKey)) {
const cached = context.positionCache.get(cackeKey)!;
if (targetPosition) Vec3.copy(targetPosition, cached[0].center);
if (targetSphere) Sphere3D.copy(targetSphere, cached[0]);
if (targetBox) Box3D.copy(targetBox, cached[1]);
return;
}
const { selection } = StructureQueryHelper.createAndRun(pivot, expr);
let box: Box3D;
let sphere: Sphere3D;
if (StructureSelection.isEmpty(selection)) {
if (targetPosition) Vec3.set(targetPosition, 0, 0, 0);
box = _EmptyBox;
sphere = _EmptySphere;
} else {
const loci = StructureSelection.toLociWithSourceUnits(selection);
const boundary = StructureElement.Loci.getBoundary(loci);
if (targetPosition) Vec3.copy(targetPosition, boundary.sphere.center);
box = boundary.box;
sphere = boundary.sphere;
}
if (targetSphere) Sphere3D.copy(targetSphere, sphere);
if (targetBox) Box3D.copy(targetBox, box);
context.positionCache.set(cackeKey, [sphere, box]);
}
function getInstances(options: PrimitivesParams | undefined): Mat4[] | undefined {
if (!options?.instances?.length) return undefined;
return options.instances.map(i => Mat4.fromArray(Mat4(), i, 0));
}
function buildPrimitiveMesh(context: PrimitiveBuilderContext, prev?: Mesh): Shape<Mesh> {
const meshBuilder = MeshBuilder.createState(1024, 1024, prev);
const state: MeshBuilderState = { groups: new GroupManager(), mesh: meshBuilder };
meshBuilder.currentGroup = -1;
for (const c of context.primitives) {
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b.builders.mesh?.(context, state, c, p);
}
const { colors, tooltips } = state.groups;
const tooltip = context.options?.tooltip ?? '';
const color = decodeColor(context.options?.color) ?? Color(0);
return Shape.create(
'Mesh',
{
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
MeshBuilder.getMesh(meshBuilder),
(g) => colors.get(g) as Color ?? color,
(g) => 1,
(g) => tooltips.get(g) ?? tooltip,
context.instances,
);
}
function buildPrimitiveLines(context: PrimitiveBuilderContext, prev?: Lines): Shape<Lines> {
const linesBuilder = LinesBuilder.create(1024, 1024, prev);
const state: LineBuilderState = { groups: new GroupManager(), lines: linesBuilder };
for (const c of context.primitives) {
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b.builders.line?.(context, state, c, p);
}
const { colors, sizes, tooltips } = state.groups;
const tooltip = context.options?.tooltip ?? '';
const color = decodeColor(context.options?.color) ?? Color(0);
return Shape.create(
'Lines',
{
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
linesBuilder.getLines(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
(g) => tooltips.get(g) ?? tooltip,
context.instances,
);
}
function buildPrimitiveLabels(context: PrimitiveBuilderContext, prev?: Text): Shape<Text> {
const labelsBuilder = TextBuilder.create(BaseLabelProps, 1024, 1024, prev);
const state: LabelBuilderState = { groups: new GroupManager(), labels: labelsBuilder };
for (const c of context.primitives) {
const p = c.params;
const b = Builders[p.kind];
if (!b) {
console.warn(`Primitive ${p.kind} not supported`);
continue;
}
b.builders.label?.(context, state, c, p);
}
const color = decodeColor(context.options?.label_color) ?? Color(0);
const { colors, sizes, tooltips } = state.groups;
return Shape.create(
'Labels',
{
kind: 'mvs-primitives',
node: context.node,
groupToNode: state.groups.groupToNodeMap,
},
labelsBuilder.getText(),
(g) => colors.get(g) as Color ?? color,
(g) => sizes.get(g) ?? 1,
(g) => tooltips.get(g) ?? '',
context.instances,
);
}
function addMeshFaces(context: PrimitiveBuilderContext, groups: GroupManager, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>, addFace: (mvsGroup: number, builderGroup: number, a: Vec3, b: Vec3, c: Vec3) => void) {
const a = Vec3.zero();
const b = Vec3.zero();
const c = Vec3.zero();
let { indices, vertices, triangle_groups } = params;
const nTriangles = Math.floor(indices.length / 3);
triangle_groups ??= range(nTriangles); // implicit grouping (triangle i = group i)
const groupSet = groups.allocateMany(node, triangle_groups);
for (let i = 0; i < nTriangles; i++) {
const mvsGroup = triangle_groups[i];
const builderGroup = groupSet.get(mvsGroup)!;
Vec3.fromArray(a, vertices, 3 * indices[3 * i]);
Vec3.fromArray(b, vertices, 3 * indices[3 * i + 1]);
Vec3.fromArray(c, vertices, 3 * indices[3 * i + 2]);
addFace(mvsGroup, builderGroup, a, b, c);
}
}
function addMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
if (!params.show_triangles) return;
const { group_colors, group_tooltips, color, tooltip } = params;
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
mesh.currentGroup = builderGroup;
MeshBuilder.addTriangle(mesh, a, b, c);
});
// this could be slightly improved by only updating color and tooltip once per group instead of once per triangle
}
function addMeshWireframe(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'mesh'>) {
if (!params.show_wireframe) return;
const width = params.wireframe_width;
const { group_colors, group_tooltips, wireframe_color, color, tooltip } = params;
addMeshFaces(context, groups, node, params, (mvsGroup, builderGroup, a, b, c) => {
groups.updateColor(builderGroup, wireframe_color ?? group_colors[mvsGroup] ?? color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? tooltip);
groups.updateSize(builderGroup, width);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
lines.add(b[0], b[1], b[2], c[0], c[1], c[2], builderGroup);
lines.add(c[0], c[1], c[2], a[0], a[1], a[2], builderGroup);
});
}
function addLines(context: PrimitiveBuilderContext, { groups, lines }: LineBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'lines'>) {
const a = Vec3.zero();
const b = Vec3.zero();
let { indices, vertices, line_groups, group_colors, group_tooltips, group_widths } = params;
const width = params.width;
const nLines = Math.floor(indices.length / 2);
line_groups ??= range(nLines); // implicit grouping (line i = group i)
const groupSet = groups.allocateMany(node, line_groups);
for (let i = 0; i < nLines; i++) {
const mvsGroup = line_groups[i];
const builderGroup = groupSet.get(mvsGroup)!;
groups.updateColor(builderGroup, group_colors[mvsGroup] ?? params.color);
groups.updateTooltip(builderGroup, group_tooltips[mvsGroup] ?? params.tooltip);
groups.updateSize(builderGroup, group_widths[mvsGroup] ?? width);
Vec3.fromArray(a, vertices, 3 * indices[2 * i]);
Vec3.fromArray(b, vertices, 3 * indices[2 * i + 1]);
lines.add(a[0], a[1], a[2], b[0], b[1], b[2], builderGroup);
}
}
function resolveLineRefs(params: PrimitiveParams<'tube' | 'distance_measurement'>, refs: Set<string>) {
addRef(params.start, refs);
addRef(params.end, refs);
}
const lStart = Vec3.zero();
const lEnd = Vec3.zero();
function addTubeMesh(context: PrimitiveBuilderContext, { groups, mesh }: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'tube'>, options?: { skipResolvePosition?: boolean }) {
if (!options?.skipResolvePosition) {
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
}
const radius = params.radius;
const cylinderProps: BasicCylinderProps = {
radiusBottom: radius,
radiusTop: radius,
topCap: true,
bottomCap: true,
};
mesh.currentGroup = groups.allocateSingle(node);
groups.updateColor(mesh.currentGroup, params.color);
groups.updateTooltip(mesh.currentGroup, params.tooltip);
if (params.dash_length) {
const dist = Vec3.distance(lStart, lEnd);
const count = Math.ceil(dist / (2 * params.dash_length));
addFixedCountDashedCylinder(mesh, lStart, lEnd, 1.0, count, true, cylinderProps);
} else {
addSimpleCylinder(mesh, lStart, lEnd, cylinderProps);
}
}
function getDistanceLabel(context: PrimitiveBuilderContext, params: PrimitiveParams<'distance_measurement'>) {
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
const dist = Vec3.distance(lStart, lEnd);
const distance = `${round(dist, 2)} Å`;
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{distance}}', distance) : distance;
return label;
}
function addDistanceMesh(context: PrimitiveBuilderContext, state: MeshBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
const tooltip = getDistanceLabel(context, params);
addTubeMesh(context, state, node, { ...params, tooltip } as any, { skipResolvePosition: true });
}
const labelPos = Vec3.zero();
function addDistanceLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'distance_measurement'>) {
const { labels, groups } = state;
resolveBasePosition(context, params.start, lStart);
resolveBasePosition(context, params.end, lEnd);
const dist = Vec3.distance(lStart, lEnd);
const distance = `${round(dist, 2)} Å`;
const label = typeof params.label_template === 'string' ? params.label_template.replace('{{distance}}', distance) : distance;
let size: number | undefined;
if (typeof params.label_size === 'number') {
size = params.label_size;
} else {
size = Math.max(dist * (params.label_auto_size_scale), params.label_auto_size_min);
}
Vec3.add(labelPos, lStart, lEnd);
Vec3.scale(labelPos, labelPos, 0.5);
const group = groups.allocateSingle(node);
groups.updateColor(group, params.label_color);
groups.updateSize(group, size);
labels.add(label, labelPos[0], labelPos[1], labelPos[2], 1.05 * (params.radius), 1, group);
}
function resolveLabelRefs(params: PrimitiveParams<'label'>, refs: Set<string>) {
addRef(params.position, refs);
}
function addPrimitiveLabel(context: PrimitiveBuilderContext, state: LabelBuilderState, node: MVSNode<'primitive'>, params: PrimitiveParams<'label'>) {
const { labels, groups } = state;
resolveBasePosition(context, params.position, labelPos);
const group = groups.allocateSingle(node);
groups.updateColor(group, params.label_color);
groups.updateSize(group, params.label_size);
labels.add(params.text, labelPos[0], labelPos[1], labelPos[2], params.label_offset, 1, group);
}

View File

@@ -0,0 +1,53 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { StructureElement } from '../../../mol-model/structure';
import { createStructureComponent } from '../../../mol-plugin-state/helpers/structure-component';
import { PluginStateTransform, PluginStateObject as SO } from '../../../mol-plugin-state/objects';
import { MolScriptBuilder } from '../../../mol-script/language/builder';
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
export const StructureSurroundingsParams = {
radius: PD.Numeric(5, { min: 0 }, { description: 'Surroundings radius in Angstroms' }),
includeSelf: PD.Boolean(true, { description: 'Include parent selection itself in the surroundings' }),
wholeResidues: PD.Boolean(true, { description: 'Include whole residues, instead of individual atoms' }),
nullIfEmpty: PD.Optional(PD.Boolean(true, { isHidden: true })),
};
export type StructureSurroundingsParams = typeof StructureSurroundingsParams;
export type StructureSurroundingsProps = PD.ValuesFor<StructureSurroundingsParams>;
export type StructureSurroundings = typeof StructureSurroundings;
export const StructureSurroundings = PluginStateTransform.BuiltIn({
name: 'structure-surroundings',
display: { name: 'Surroundings', description: 'Surroundings of a structure component.' },
from: SO.Molecule.Structure,
to: SO.Molecule.Structure,
params: StructureSurroundingsParams,
})({
apply({ a, params, cache }) {
const struct = a.data;
const rootStruct = struct.parent ?? struct;
const targetBundle = StructureElement.Bundle.fromSubStructure(rootStruct, struct);
const targetExpr = StructureElement.Bundle.toExpression(targetBundle);
let surroundingsExpr = MolScriptBuilder.struct.modifier.includeSurroundings({
0: targetExpr,
radius: params.radius,
'as-whole-residues': params.wholeResidues,
});
if (!params.includeSelf) {
surroundingsExpr = MolScriptBuilder.struct.modifier.exceptBy({
0: surroundingsExpr,
by: targetExpr,
});
}
return createStructureComponent(rootStruct, { label: `Surroundings (${params.radius} Å)`, type: { name: 'expression', params: surroundingsExpr }, nullIfEmpty: params.nullIfEmpty }, cache as any);
},
dispose({ b }) {
b?.data.customPropertyDescriptors.dispose();
}
});

View File

@@ -311,6 +311,7 @@ export function rowToExpression(row: MVSAnnotationRow): Expression {
/** Convert multiple annotation rows into a MolScript expression.
* (with union semantics, i.e. an atom qualifies if it qualifies for at least one of the rows) */
export function rowsToExpression(rows: readonly MVSAnnotationRow[]): Expression {
if (rows.length === 1) return rowToExpression(rows[0]);
return unionExpression(rows.map(rowToExpression));
}

View File

@@ -1,10 +1,12 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { hashString } from '../../../mol-data/util';
import { StateObject } from '../../../mol-state';
import { Color } from '../../../mol-util/color';
import { ColorNames } from '../../../mol-util/color/names';
@@ -97,8 +99,8 @@ export type ElementOfSet<S> = S extends Set<infer T> ? T : never
/** Convert `colorString` (either X11 color name like 'magenta' or hex code like '#ff00ff') to Color.
* Return `undefined` if `colorString` cannot be converted. */
export function decodeColor(colorString: string | undefined): Color | undefined {
if (colorString === undefined) return undefined;
export function decodeColor(colorString: string | undefined | null): Color | undefined {
if (colorString === undefined || colorString === null) return undefined;
let result: Color | undefined;
if (HexColor.is(colorString)) {
if (colorString.length === 4) {
@@ -124,4 +126,38 @@ export const HexColor = {
is(str: any): str is HexColor {
return typeof str === 'string' && hexColorRegex.test(str);
},
};
};
/** Named color string, e.g. 'red' */
export type ColorName = keyof ColorNames
export const ColorName = {
/** Decide if a string is a valid named color string */
is(str: any): str is ColorName {
return str in ColorNames;
},
};
export function collectMVSReferences<T extends StateObject.Ctor>(type: T[], dependencies: Record<string, StateObject>): Record<string, StateObject.From<T>['data']> {
const ret: any = {};
for (const key of Object.keys(dependencies)) {
const o = dependencies[key];
let okType = false;
for (const t of type) {
if (t.is(o)) {
okType = true;
break;
}
}
if (!okType || !o.tags) continue;
for (const tag of o.tags) {
if (tag.startsWith('mvs-ref:')) {
ret[tag.substring(8)] = o.data;
break;
}
}
}
return ret;
}

View File

@@ -0,0 +1,55 @@
/**
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { StateTransforms } from '../../../mol-plugin-state/transforms';
import { StructureSurroundings } from '../components/surroundings';
import { MolstarLoadingExtension } from '../load';
import { UpdateTarget } from '../load-generic';
import { getCustomProps } from '../tree/generic/tree-schema';
const DefaultNonCovalentInteractionRadius = 5;
export const NonCovalentInteractionsExtension: MolstarLoadingExtension<{}> = {
id: 'wwpdb/non-covalent-interactions',
description: 'Allow showing non-covalent interactions around components with molstar_show_non_covalent_interactions additional property',
createExtensionContext: () => ({}),
action: (updateTarget, node, context, extContext) => {
if (node.kind !== 'component' && node.kind !== 'component_from_uri' && node.kind !== 'component_from_source') return;
type CustomProps = {
molstar_show_non_covalent_interactions?: boolean,
molstar_non_covalent_interactions_radius_ang?: number,
};
const customProps = getCustomProps<CustomProps>(node);
if (!customProps.molstar_show_non_covalent_interactions) return undefined;
const surroundings = UpdateTarget.apply(updateTarget, StructureSurroundings, {
radius: customProps.molstar_non_covalent_interactions_radius_ang ?? DefaultNonCovalentInteractionRadius,
includeSelf: true,
wholeResidues: true,
nullIfEmpty: false,
});
// Bubble on target
UpdateTarget.apply(updateTarget, StateTransforms.Representation.StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.22, sizeAspectRatio: 0.73, adjustCylinderLength: true, xrayShaded: true, aromaticBonds: false, multipleBonds: 'off', excludeTypes: ['hydrogen-bond', 'metal-coordination'] } },
colorTheme: { name: 'element-symbol', params: {} },
sizeTheme: { name: 'physical', params: {} },
});
// Ball-and-stick on surrounding
UpdateTarget.apply(surroundings, StateTransforms.Representation.StructureRepresentation3D, {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.16, excludeTypes: ['hydrogen-bond', 'metal-coordination'] } },
colorTheme: { name: 'element-symbol', params: {} },
sizeTheme: { name: 'physical', params: {} },
});
// Non-covalent interactions
UpdateTarget.apply(surroundings, StateTransforms.Representation.StructureRepresentation3D, {
type: { name: 'interactions', params: {} },
colorTheme: { name: 'interaction-type', params: {} },
sizeTheme: { name: 'uniform', params: {} },
});
},
};

View File

@@ -0,0 +1,218 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../mol-plugin/context';
import { PluginState } from '../../mol-plugin/state';
import { State, StateBuilder, StateObject, StateObjectSelector, StateTransform, StateTransformer, StateTree } from '../../mol-state';
import { UUID } from '../../mol-util';
import { stringHash } from './helpers/utils';
import { Kind, Subtree, SubtreeOfKind, Tree } from './tree/generic/tree-schema';
import { dfs } from './tree/generic/tree-utils';
/** Function responsible for loading a tree node `node` into Mol*.
* Should apply changes within `updateParent.update` but not commit them.
* Should modify `context` accordingly, if it is needed for loading other nodes later.
* `updateParent.selector` is the result of loading the node's parent into Mol* state hierarchy (or the hierarchy root in case of root node). */
export type LoadingAction<TNode extends Tree, TContext> = (updateParent: UpdateTarget, node: TNode, context: TContext) => UpdateTarget | undefined
/** Loading actions for loading a tree into Mol*, per node kind. */
export type LoadingActions<TTree extends Tree, TContext> = { [kind in Kind<Subtree<TTree>>]?: LoadingAction<SubtreeOfKind<TTree, kind>, TContext> }
/** Type for defining custom behavior when loading trees, usually based on node custom properties. */
export interface LoadingExtension<TTree extends Tree, TContext, TExtensionContext> {
id: string,
description: string,
/** Runs before the tree is loaded */
createExtensionContext: (tree: TTree, context: TContext) => TExtensionContext,
/** Runs after the tree is loaded */
disposeExtensionContext?: (extensionContext: TExtensionContext, tree: TTree, context: TContext) => void,
/** Runs on every node of the tree */
action: (updateTarget: UpdateTarget, node: Subtree<TTree>, context: TContext, extensionContext: TExtensionContext) => void,
}
/** Load a tree into Mol*, by applying loading actions in DFS order and then commiting at once.
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state. */
export async function loadTree<TTree extends Tree, TContext>(
plugin: PluginContext,
tree: TTree,
loadingActions: LoadingActions<TTree, TContext>,
context: TContext,
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
) {
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
await UpdateTarget.commit(updateRoot);
}
export function loadTreeVirtual<TTree extends Tree, TContext>(
plugin: PluginContext,
tree: TTree,
loadingActions: LoadingActions<TTree, TContext>,
context: TContext,
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
) {
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
loadTreeInUpdate(updateRoot, tree, loadingActions, context, options);
const stateTree: StateTree = updateRoot.update.getTree();
const stateSnapshot: State.Snapshot = { tree: StateTree.toJSON(stateTree) };
const pluginStateSnapshot: PluginState.Snapshot = { id: UUID.create22(), data: stateSnapshot };
return pluginStateSnapshot;
}
function loadTreeInUpdate<TTree extends Tree, TContext>(updateRoot: UpdateTarget,
tree: TTree,
loadingActions: LoadingActions<TTree, TContext>,
context: TContext,
options?: { replaceExisting?: boolean, extensions?: LoadingExtension<TTree, TContext, any>[] }
) {
const mapping = new Map<Subtree<TTree>, UpdateTarget | undefined>();
if (options?.replaceExisting) {
UpdateTarget.deleteChildren(updateRoot);
}
const extensionContexts = (options?.extensions ?? []).map(ext => ({ ext, extCtx: ext.createExtensionContext(tree, context) }));
const mvsRefMap = new Map<string, string>();
dfs<TTree>(tree, (node, parent) => {
const kind: Kind<typeof node> = node.kind;
let msNode: UpdateTarget | undefined;
const updateParent = parent ? mapping.get(parent) : updateRoot;
const action = loadingActions[kind] as LoadingAction<typeof node, TContext> | undefined;
if (action) {
if (updateParent) {
msNode = action(updateParent, node, context);
if (msNode && node.ref) {
UpdateTarget.tag(msNode, mvsRefTags(node.ref));
mvsRefMap.set(node.ref, msNode.selector.ref);
}
mapping.set(node, msNode);
} else {
console.warn(`No target found for this "${node.kind}" node`);
return;
}
}
if (updateParent) {
for (const { ext, extCtx } of extensionContexts) {
ext.action(msNode ?? updateParent, node, context, extCtx);
}
}
});
for (const target of updateRoot.targetManager.allTargets) {
UpdateTarget.dependsOn(target, mvsRefMap);
}
extensionContexts.forEach(e => e.ext.disposeExtensionContext?.(e.extCtx, tree, context));
}
/** A wrapper for updating Mol* state, while using deterministic transform refs.
* ```
* updateTarget = UpdateTarget.create(plugin); // like update = plugin.build();
* UpdateTarget.apply(updateTarget, transformer, params); // like update.to(selector).apply(transformer, params);
* await UpdateTarget.commit(updateTarget); // like await update.commit();
* ```
*/
export interface UpdateTarget {
readonly update: StateBuilder.Root,
readonly selector: StateObjectSelector,
readonly targetManager: TargetManager,
readonly mvsDependencyRefs: Set<string>,
}
export const UpdateTarget = {
/** Create a new update, with `selector` pointing to the root. */
create(plugin: PluginContext, replaceExisting: boolean): UpdateTarget {
const update = plugin.build();
const msTarget = update.toRoot().selector;
return { update, selector: msTarget, targetManager: new TargetManager(plugin, replaceExisting), mvsDependencyRefs: new Set() };
},
/** Add a child node to `target.selector`, return a new `UpdateTarget` pointing to the new child. */
apply<A extends StateObject, B extends StateObject, P extends {}>(target: UpdateTarget, transformer: StateTransformer<A, B, P>, params?: Partial<P>, options?: Partial<StateTransform.Options>): UpdateTarget {
let refSuffix: string = transformer.id;
if (transformer.id === StructureRepresentation3D.id) {
const reprType = (params as any)?.type?.name ?? '';
refSuffix += `:${reprType}`;
}
const ref = target.targetManager.getChildRef(target.selector, refSuffix);
const msResult = target.update.to(target.selector).apply(transformer, params, { ...options, ref }).selector;
const result: UpdateTarget = { ...target, selector: msResult, mvsDependencyRefs: new Set() };
target.targetManager.allTargets.push(result);
return result;
},
setMvsDependencies(target: UpdateTarget, refs: string[] | Set<string>): UpdateTarget {
refs.forEach(ref => target.mvsDependencyRefs.add(ref));
return target;
},
dependsOn(target: UpdateTarget, mapping: Map<string, string>): UpdateTarget {
if (!target.mvsDependencyRefs.size) return target;
const dependsOn = Array.from(target.mvsDependencyRefs).map(d => mapping.get(d)!).filter(d => d);
if (!dependsOn.length) return target;
target.update.to(target.selector).dependsOn(dependsOn);
return target;
},
/** Add tags to `target.selector` */
tag(target: UpdateTarget, tags: string[]): UpdateTarget {
if (tags.length > 0) {
target.update.to(target.selector).tag(tags);
}
return target;
},
/** Delete all children of `target.selector`. */
deleteChildren(target: UpdateTarget): UpdateTarget {
const children = target.update.currentTree.children.get(target.selector.ref);
children.forEach(child => target.update.delete(child));
return target;
},
/** Commit all changes done in the current update. */
commit(target: UpdateTarget): Promise<void> {
return target.update.commit();
},
};
/** Manages transform refs in a deterministic way. Uses refs like !mvs:3ce3664304d32c5d:0 */
class TargetManager {
/** For each hash (e.g. 3ce3664304d32c5d), store the number of already used refs with that hash. */
private _counter: Record<string, number> = {};
constructor(plugin: PluginContext, replaceExisting: boolean) {
if (!replaceExisting) {
plugin.state.data.cells.forEach(cell => {
const ref = cell.transform.ref;
if (ref.startsWith('!mvs:')) {
const [_, hash, idNumber] = ref.split(':');
const nextIdNumber = parseInt(idNumber) + 1;
if (nextIdNumber > (this._counter[hash] ?? 0)) {
this._counter[hash] = nextIdNumber;
}
}
});
}
}
/** Return ref for a new node with given `hash`; update the counter accordingly. */
private nextRef(hash: string): string {
this._counter[hash] ??= 0;
const idNumber = this._counter[hash]++;
return `!mvs:${hash}:${idNumber}`;
}
/** Return ref for a new node based on parent and desired suffix. */
getChildRef(parent: StateObjectSelector, suffix: string): string {
const hashBase = parent.ref.replace(/^!mvs:/, '') + ':' + suffix;
const hash = stringHash(hashBase);
const result = this.nextRef(hash);
return result;
}
readonly allTargets: UpdateTarget[] = [];
}
/** Create node tags based of MVS node.ref */
export function mvsRefTags(mvsNodeRef: string | undefined): string[] {
if (mvsNodeRef === undefined) return [];
else return [`mvs-ref:${mvsNodeRef}`];
}

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -8,8 +8,7 @@ import { Mat3, Mat4, Vec3 } from '../../mol-math/linear-algebra';
import { StructureComponentParams } from '../../mol-plugin-state/helpers/structure-component';
import { StructureFromModel, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginContext } from '../../mol-plugin/context';
import { StateBuilder, StateObject, StateObjectSelector, StateTransform, StateTransformer } from '../../mol-state';
import { StateTransformer } from '../../mol-state';
import { arrayDistinct } from '../../mol-util/array';
import { canonicalJsonString } from '../../mol-util/json';
import { stringToWords } from '../../mol-util/string';
@@ -25,122 +24,9 @@ import { SelectorAll } from './components/selector';
import { rowToExpression, rowsToExpression } from './helpers/selections';
import { ElementOfSet, decodeColor, isDefined, stringHash } from './helpers/utils';
import { MolstarLoadingContext } from './load';
import { Kind, ParamsOfKind, SubTree, SubTreeOfKind, Tree, getChildren } from './tree/generic/tree-schema';
import { Subtree, getChildren } from './tree/generic/tree-schema';
import { dfs, formatObject } from './tree/generic/tree-utils';
import { MolstarKind, MolstarNode, MolstarTree } from './tree/molstar/molstar-tree';
import { DefaultColor } from './tree/mvs/mvs-defaults';
/** Function responsible for loading a tree node `node` into Mol*.
* Should apply changes within `updateParent.update` but not commit them.
* Should modify `context` accordingly, if it is needed for loading other nodes later.
* `updateParent.selector` is the result of loading the node's parent into Mol* state hierarchy (or the hierarchy root in case of root node). */
export type LoadingAction<TNode extends Tree, TContext> = (updateParent: UpdateTarget, node: TNode, context: TContext) => UpdateTarget | undefined
/** Loading actions for loading a tree into Mol*, per node kind. */
export type LoadingActions<TTree extends Tree, TContext> = { [kind in Kind<SubTree<TTree>>]?: LoadingAction<SubTreeOfKind<TTree, kind>, TContext> }
/** Load a tree into Mol*, by applying loading actions in DFS order and then commiting at once.
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state. */
export async function loadTree<TTree extends Tree, TContext>(plugin: PluginContext, tree: TTree, loadingActions: LoadingActions<TTree, TContext>, context: TContext, options?: { replaceExisting?: boolean }) {
const mapping = new Map<SubTree<TTree>, UpdateTarget | undefined>();
const updateRoot: UpdateTarget = UpdateTarget.create(plugin, options?.replaceExisting ?? false);
if (options?.replaceExisting) {
UpdateTarget.deleteChildren(updateRoot);
}
dfs<TTree>(tree, (node, parent) => {
const kind: Kind<typeof node> = node.kind;
const action = loadingActions[kind] as LoadingAction<typeof node, TContext> | undefined;
if (action) {
const updateParent = parent ? mapping.get(parent) : updateRoot;
if (updateParent) {
const msNode = action(updateParent, node, context);
mapping.set(node, msNode);
} else {
console.warn(`No target found for this "${node.kind}" node`);
return;
}
}
});
await UpdateTarget.commit(updateRoot);
}
/** A wrapper for updating Mol* state, while using deterministic transform refs.
* ```
* updateTarget = UpdateTarget.create(plugin); // like update = plugin.build();
* UpdateTarget.apply(updateTarget, transformer, params); // like update.to(selector).apply(transformer, params);
* await UpdateTarget.commit(updateTarget); // like await update.commit();
* ```
*/
export interface UpdateTarget {
readonly update: StateBuilder.Root,
readonly selector: StateObjectSelector,
readonly refManager: RefManager,
}
export const UpdateTarget = {
/** Create a new update, with `selector` pointing to the root. */
create(plugin: PluginContext, replaceExisting: boolean): UpdateTarget {
const update = plugin.build();
const msTarget = update.toRoot().selector;
const refManager = new RefManager(plugin, replaceExisting);
return { update, selector: msTarget, refManager };
},
/** Add a child node to `target.selector`, return a new `UpdateTarget` pointing to the new child. */
apply<A extends StateObject, B extends StateObject, P extends {}>(target: UpdateTarget, transformer: StateTransformer<A, B, P>, params?: Partial<P>, options?: Partial<StateTransform.Options>): UpdateTarget {
let refSuffix: string = transformer.id;
if (transformer.id === StructureRepresentation3D.id) {
const reprType = (params as any)?.type?.name ?? '';
refSuffix += `:${reprType}`;
}
const ref = target.refManager.getChildRef(target.selector, refSuffix);
const msResult = target.update.to(target.selector).apply(transformer, params, { ...options, ref }).selector;
return { ...target, selector: msResult };
},
/** Delete all children of `target.selector`. */
deleteChildren(target: UpdateTarget): UpdateTarget {
const children = target.update.currentTree.children.get(target.selector.ref);
children.forEach(child => target.update.delete(child));
return target;
},
/** Commit all changes done in the current update. */
commit(target: UpdateTarget): Promise<void> {
return target.update.commit();
},
};
/** Manages transform refs in a deterministic way. Uses refs like !mvs:3ce3664304d32c5d:0 */
class RefManager {
/** For each hash (e.g. 3ce3664304d32c5d), store the number of already used refs with that hash. */
private _counter: Record<string, number> = {};
constructor(plugin: PluginContext, replaceExisting: boolean) {
if (!replaceExisting) {
plugin.state.data.cells.forEach(cell => {
const ref = cell.transform.ref;
if (ref.startsWith('!mvs:')) {
const [_, hash, idNumber] = ref.split(':');
const nextIdNumber = parseInt(idNumber) + 1;
if (nextIdNumber > (this._counter[hash] ?? 0)) {
this._counter[hash] = nextIdNumber;
}
}
});
}
}
/** Return ref for a new node with given `hash`; update the counter accordingly. */
private nextRef(hash: string): string {
this._counter[hash] ??= 0;
const idNumber = this._counter[hash]++;
return `!mvs:${hash}:${idNumber}`;
}
/** Return ref for a new node based on parent and desired suffix. */
getChildRef(parent: StateObjectSelector, suffix: string): string {
const hashBase = parent.ref.replace(/^!mvs:/, '') + ':' + suffix;
const hash = stringHash(hashBase);
const result = this.nextRef(hash);
return result;
}
}
import { MolstarKind, MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree } from './tree/molstar/molstar-tree';
export const AnnotationFromUriKinds = new Set(['color_from_uri', 'component_from_uri', 'label_from_uri', 'tooltip_from_uri'] satisfies MolstarKind[]);
@@ -149,6 +35,9 @@ export type AnnotationFromUriKind = ElementOfSet<typeof AnnotationFromUriKinds>
export const AnnotationFromSourceKinds = new Set(['color_from_source', 'component_from_source', 'label_from_source', 'tooltip_from_source'] satisfies MolstarKind[]);
export type AnnotationFromSourceKind = ElementOfSet<typeof AnnotationFromSourceKinds>
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';
/** Return a 4x4 matrix representing a rotation followed by a translation */
export function transformFromRotationTranslation(rotation: number[] | null | undefined, translation: number[] | null | undefined): Mat4 {
@@ -184,7 +73,7 @@ const _tmpVecY = Vec3();
const _tmpVecZ = Vec3();
/** Create an array of props for `TransformStructureConformation` transformers from all 'transform' nodes applied to a 'structure' node. */
export function transformProps(node: SubTreeOfKind<MolstarTree, 'structure'>): StateTransformer.Params<TransformStructureConformation>[] {
export function transformProps(node: MolstarSubtree<'structure'>): StateTransformer.Params<TransformStructureConformation>[] {
const result = [] as StateTransformer.Params<TransformStructureConformation>[];
const transforms = getChildren(node).filter(c => c.kind === 'transform') as MolstarNode<'transform'>[];
for (const transform of transforms) {
@@ -196,7 +85,7 @@ export function transformProps(node: SubTreeOfKind<MolstarTree, 'structure'>): S
}
/** Collect distinct annotation specs from all nodes in `tree` and set `context.annotationMap[node]` to respective annotationIds */
export function collectAnnotationReferences(tree: SubTree<MolstarTree>, context: MolstarLoadingContext): MVSAnnotationSpec[] {
export function collectAnnotationReferences(tree: Subtree<MolstarTree>, context: MolstarLoadingContext): MVSAnnotationSpec[] {
const distinctSpecs: { [key: string]: MVSAnnotationSpec } = {};
dfs(tree, node => {
let spec: Omit<MVSAnnotationSpec, 'id'> | undefined = undefined;
@@ -210,7 +99,7 @@ export function collectAnnotationReferences(tree: SubTree<MolstarTree>, context:
if (spec) {
const key = canonicalJsonString(spec as any);
distinctSpecs[key] ??= { ...spec, id: stringHash(key) };
(context.annotationMap ??= new Map()).set(node, distinctSpecs[key].id);
context.annotationMap.set(node as MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, distinctSpecs[key].id);
}
});
return Object.values(distinctSpecs);
@@ -224,11 +113,11 @@ function blockSpec(header: string | null | undefined, index: number | null | und
}
/** Collect annotation tooltips from all nodes in `tree` and map them to annotationIds. */
export function collectAnnotationTooltips(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): MVSAnnotationTooltipsProps['tooltips'] {
export function collectAnnotationTooltips(tree: MolstarSubtree<'structure'>, context: MolstarLoadingContext): MVSAnnotationTooltipsProps['tooltips'] {
const annotationTooltips: MVSAnnotationTooltipsProps['tooltips'] = [];
dfs(tree, node => {
if (node.kind === 'tooltip_from_uri' || node.kind === 'tooltip_from_source') {
const annotationId = context.annotationMap?.get(node);
const annotationId = context.annotationMap.get(node);
if (annotationId) {
annotationTooltips.push({ annotationId, fieldName: node.params.field_name });
};
@@ -237,7 +126,7 @@ export function collectAnnotationTooltips(tree: SubTreeOfKind<MolstarTree, 'stru
return arrayDistinct(annotationTooltips);
}
/** Collect inline tooltips from all nodes in `tree`. */
export function collectInlineTooltips(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): CustomTooltipsProps['tooltips'] {
export function collectInlineTooltips(tree: MolstarSubtree<'structure'>, context: MolstarLoadingContext): CustomTooltipsProps['tooltips'] {
const inlineTooltips: CustomTooltipsProps['tooltips'] = [];
dfs(tree, (node, parent) => {
if (node.kind === 'tooltip') {
@@ -263,7 +152,7 @@ export function collectInlineTooltips(tree: SubTreeOfKind<MolstarTree, 'structur
return inlineTooltips;
}
/** Collect inline labels from all nodes in `tree`. */
export function collectInlineLabels(tree: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): CustomLabelTextProps['items'] {
export function collectInlineLabels(tree: MolstarSubtree<'structure'>, context: MolstarLoadingContext): CustomLabelTextProps['items'] {
const inlineLabels: CustomLabelTextProps['items'] = [];
dfs(tree, (node, parent) => {
if (node.kind === 'label') {
@@ -300,7 +189,9 @@ export function collectInlineLabels(tree: SubTreeOfKind<MolstarTree, 'structure'
}
/** Return `true` for components nodes which only serve for tooltip placement (not to be created in the MolStar object hierarchy) */
export function isPhantomComponent(node: SubTreeOfKind<MolstarTree, 'component' | 'component_from_uri' | 'component_from_source'>) {
export function isPhantomComponent(node: MolstarSubtree<'component' | 'component_from_uri' | 'component_from_source'>) {
if (node.ref !== undefined) return false;
if (node.custom !== undefined && Object.keys(node.custom).length > 0) return false;
return node.children && node.children.every(child => child.kind === 'tooltip' || child.kind === 'label');
// These nodes could theoretically be removed when converting MVS to Molstar tree, but would get very tricky if we allow nested components
}
@@ -343,7 +234,7 @@ export function structureProps(node: MolstarNode<'structure'>): StateTransformer
}
/** Create value for `type` prop for `StructureComponent` transformer based on a MVS selector. */
export function componentPropsFromSelector(selector?: ParamsOfKind<MolstarTree, 'component'>['selector']): StructureComponentParams['type'] {
export function componentPropsFromSelector(selector?: MolstarNodeParams<'component'>['selector']): StructureComponentParams['type'] {
if (selector === undefined) {
return SelectorAll;
} else if (typeof selector === 'string') {
@@ -356,7 +247,7 @@ export function componentPropsFromSelector(selector?: ParamsOfKind<MolstarTree,
}
/** Return a pretty name for a value of selector param, e.g. "protein" -> 'Protein', {label_asym_id: "A"} -> 'Custom Selection: {label_asym_id: "A"}' */
export function prettyNameFromSelector(selector?: ParamsOfKind<MolstarTree, 'component'>['selector']): string {
export function prettyNameFromSelector(selector?: MolstarNodeParams<'component'>['selector']): string {
if (selector === undefined) {
return 'All';
} else if (typeof selector === 'string') {
@@ -370,7 +261,7 @@ export function prettyNameFromSelector(selector?: ParamsOfKind<MolstarTree, 'com
/** Create props for `StructureRepresentation3D` transformer from a label_from_* node. */
export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from_source'>, context: MolstarLoadingContext): Partial<StateTransformer.Params<StructureRepresentation3D>> {
const annotationId = context.annotationMap?.get(node);
const annotationId = context.annotationMap.get(node);
const fieldName = node.params.field_name;
const nearestReprNode = context.nearestReprMap?.get(node);
return {
@@ -381,7 +272,7 @@ export function labelFromXProps(node: MolstarNode<'label_from_uri' | 'label_from
/** Create props for `AnnotationStructureComponent` transformer from a component_from_* node. */
export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'component_from_source'>, context: MolstarLoadingContext): Partial<MVSAnnotationStructureComponentProps> {
const annotationId = context.annotationMap?.get(node);
const annotationId = context.annotationMap.get(node);
const { field_name, field_values } = node.params;
return {
annotationId,
@@ -392,19 +283,20 @@ export function componentFromXProps(node: MolstarNode<'component_from_uri' | 'co
}
/** Create props for `StructureRepresentation3D` transformer from a representation node. */
export function representationProps(params: ParamsOfKind<MolstarTree, 'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
switch (params.type) {
export function representationProps(node: MolstarSubtree<'representation'>): Partial<StateTransformer.Params<StructureRepresentation3D>> {
const alpha = alphaForNode(node);
switch (node.params.type) {
case 'cartoon':
return {
type: { name: 'cartoon', params: {} },
type: { name: 'cartoon', params: { alpha } },
};
case 'ball_and_stick':
return {
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5 } },
type: { name: 'ball-and-stick', params: { sizeFactor: 0.5, sizeAspectRatio: 0.5, alpha } },
};
case 'surface':
return {
type: { name: 'molecular-surface', params: {} },
type: { name: 'molecular-surface', params: { alpha } },
sizeTheme: { name: 'physical', params: { scale: 1 } },
};
default:
@@ -412,8 +304,17 @@ export function representationProps(params: ParamsOfKind<MolstarTree, 'represent
}
}
/** Create value for `type.params.alpha` prop for `StructureRepresentation3D` transformer from a representation node based on 'opacity' nodes in its subtree. */
export function alphaForNode(node: MolstarSubtree<'representation'>): number {
const children = getChildren(node).filter(c => c.kind === 'opacity');
if (children.length > 0) {
return children[children.length - 1].params.opacity;
} else {
return 1;
}
}
/** Create value for `colorTheme` prop for `StructureRepresentation3D` transformer from a representation node based on color* nodes in its subtree. */
export function colorThemeForNode(node: SubTreeOfKind<MolstarTree, 'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] {
export function colorThemeForNode(node: MolstarSubtree<'color' | 'color_from_uri' | 'color_from_source' | 'representation'> | undefined, context: MolstarLoadingContext): StateTransformer.Params<StructureRepresentation3D>['colorTheme'] {
if (node?.kind === 'representation') {
const children = getChildren(node).filter(c => c.kind === 'color' || c.kind === 'color_from_uri' || c.kind === 'color_from_source') as MolstarNode<'color' | 'color_from_uri' | 'color_from_source'>[];
if (children.length === 0) {
@@ -439,7 +340,7 @@ export function colorThemeForNode(node: SubTreeOfKind<MolstarTree, 'color' | 'co
switch (node?.kind) {
case 'color_from_uri':
case 'color_from_source':
annotationId = context.annotationMap?.get(node);
annotationId = context.annotationMap.get(node);
fieldName = node.params.field_name;
break;
case 'color':

View File

@@ -6,25 +6,29 @@
* @author Aliaksei Chareshneu <chareshneu.tech@gmail.com>
*/
import { PluginStateSnapshotManager } from '../../mol-plugin-state/manager/snapshots';
import { Download, ParseCif } from '../../mol-plugin-state/transforms/data';
import { CustomModelProperties, CustomStructureProperties, ModelFromTrajectory, StructureComponent, StructureFromModel, TrajectoryFromMmCif, TrajectoryFromPDB, TransformStructureConformation } from '../../mol-plugin-state/transforms/model';
import { StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../mol-plugin-state/transforms/representation';
import { PluginCommands } from '../../mol-plugin/commands';
import { PluginContext } from '../../mol-plugin/context';
import { StateObjectSelector } from '../../mol-state';
import { MolViewSpec } from './behavior';
import { setCamera, setCanvas, setFocus, suppressCameraAutoreset } from './camera';
import { createPluginStateSnapshotCamera, modifyCanvasProps, setCamera, setCanvas, setFocus, suppressCameraAutoreset } from './camera';
import { MVSAnnotationsProvider } from './components/annotation-prop';
import { MVSAnnotationStructureComponent } from './components/annotation-structure-component';
import { MVSAnnotationTooltipsProvider } from './components/annotation-tooltips-prop';
import { CustomLabelProps, CustomLabelRepresentationProvider } from './components/custom-label/representation';
import { CustomTooltipsProvider } from './components/custom-tooltips-prop';
import { IsMVSModelProps, IsMVSModelProvider } from './components/is-mvs-model-prop';
import { AnnotationFromSourceKind, AnnotationFromUriKind, LoadingActions, UpdateTarget, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, loadTree, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
import { MVSData } from './mvs-data';
import { ParamsOfKind, SubTreeOfKind, validateTree } from './tree/generic/tree-schema';
import { getPrimitiveStructureRefs, MVSBuildPrimitiveShape, MVSDownloadPrimitiveData, MVSInlinePrimitiveData } from './components/primitives';
import { NonCovalentInteractionsExtension } from './load-extensions/non-covalent-interactions';
import { LoadingActions, LoadingExtension, loadTree, loadTreeVirtual, UpdateTarget } from './load-generic';
import { AnnotationFromSourceKind, AnnotationFromUriKind, collectAnnotationReferences, collectAnnotationTooltips, collectInlineLabels, collectInlineTooltips, colorThemeForNode, componentFromXProps, componentPropsFromSelector, isPhantomComponent, labelFromXProps, makeNearestReprMap, prettyNameFromSelector, representationProps, structureProps, transformProps } from './load-helpers';
import { MVSData, SnapshotMetadata } from './mvs-data';
import { validateTree } from './tree/generic/tree-schema';
import { convertMvsToMolstar, mvsSanityCheck } from './tree/molstar/conversion';
import { MolstarNode, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
import { MolstarNode, MolstarNodeParams, MolstarSubtree, MolstarTree, MolstarTreeSchema } from './tree/molstar/molstar-tree';
import { MVSTreeSchema } from './tree/mvs/mvs-tree';
@@ -32,17 +36,41 @@ import { MVSTreeSchema } from './tree/mvs/mvs-tree';
* If `options.replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state.
* If `options.keepCamera`, ignore any camera positioning from the MVS state and keep the current camera position instead.
* If `options.sanityChecks`, run some sanity checks and print potential issues to the console.
* If `options.extensions` is provided, apply specified set of MVS-loading extensions (not a part of standard MVS specification); default: apply all builtin extensions; use `extensions: []` to avoid applying builtin extensions.
* `options.sourceUrl` serves as the base for resolving relative URLs/URIs and may itself be relative to the window URL. */
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
export async function loadMVS(plugin: PluginContext, data: MVSData, options: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[], sanityChecks?: boolean, sourceUrl?: string, doNotReportErrors?: boolean } = {}) {
plugin.errorContext.clear('mvs');
try {
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
// console.log(`MVS tree:\n${MVSData.toPrettyString(data)}`)
validateTree(MVSTreeSchema, data.root, 'MVS');
if (options.sanityChecks) mvsSanityCheck(data.root);
const molstarTree = convertMvsToMolstar(data.root, options.sourceUrl);
// console.log(`Converted MolStar tree:\n${MVSData.toPrettyString({ root: molstarTree, metadata: { version: 'x', timestamp: 'x' } })}`)
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
await loadMolstarTree(plugin, molstarTree, options);
if (data.kind === 'multiple') {
const entries: PluginStateSnapshotManager.Entry[] = [];
for (let i = 0; i < data.snapshots.length; i++) {
const snapshot = data.snapshots[i];
const previousSnapshot = i > 0 ? data.snapshots[i - 1] : data.snapshots[data.snapshots.length - 1];
validateTree(MVSTreeSchema, snapshot.root, 'MVS');
if (options.sanityChecks) mvsSanityCheck(snapshot.root);
const molstarTree = convertMvsToMolstar(snapshot.root, options.sourceUrl);
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
const entry = molstarTreeToEntry(plugin, molstarTree, { ...snapshot.metadata, previousTransitionDurationMs: previousSnapshot.metadata.transition_duration_ms }, options);
entries.push(entry);
}
plugin.managers.snapshot.clear();
for (const entry of entries) {
plugin.managers.snapshot.add(entry);
}
if (entries.length > 0) {
await PluginCommands.State.Snapshots.Apply(plugin, { id: entries[0].snapshot.id });
}
} else {
validateTree(MVSTreeSchema, data.root, 'MVS');
if (options.sanityChecks) mvsSanityCheck(data.root);
const molstarTree = convertMvsToMolstar(data.root, options.sourceUrl);
// console.log(`Converted MolStar tree:\n${MVSData.toPrettyString({ root: molstarTree, metadata: { version: 'x', timestamp: 'x' } })}`)
validateTree(MolstarTreeSchema, molstarTree, 'Converted Molstar');
await loadMolstarTree(plugin, molstarTree, options);
}
} catch (err) {
plugin.log.error(`${err}`);
throw err;
@@ -64,38 +92,66 @@ export async function loadMVS(plugin: PluginContext, data: MVSData, options: { r
/** Load a `MolstarTree` into the Mol* plugin.
* If `replaceExisting`, remove all objects in the current Mol* state; otherwise add to the current state. */
async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
async function loadMolstarTree(plugin: PluginContext, tree: MolstarTree, options?: { replaceExisting?: boolean, keepCamera?: boolean, extensions?: MolstarLoadingExtension<any>[] }) {
const mvsExtensionLoaded = plugin.state.hasBehavior(MolViewSpec);
if (!mvsExtensionLoaded) throw new Error('MolViewSpec extension is not loaded.');
const context: MolstarLoadingContext = {};
const context = MolstarLoadingContext.create();
await loadTree(plugin, tree, MolstarLoadingActions, context, options);
await loadTree(plugin, tree, MolstarLoadingActions, context, { ...options, extensions: options?.extensions ?? BuiltinLoadingExtensions });
setCanvas(plugin, context.canvas);
if (options?.keepCamera) {
await suppressCameraAutoreset(plugin);
} else {
if (context.focus?.kind === 'camera') {
await setCamera(plugin, context.focus.params);
} else if (context.focus?.kind === 'focus') {
await setFocus(plugin, context.focus.focusTarget, context.focus.params);
if (context.camera.cameraParams !== undefined) {
await setCamera(plugin, context.camera.cameraParams);
} else {
await setFocus(plugin, undefined, undefined);
await setFocus(plugin, context.camera.focuses); // This includes implicit camera (i.e. no 'camera' or 'focus' nodes)
}
}
}
function molstarTreeToEntry(plugin: PluginContext, tree: MolstarTree, metadata: SnapshotMetadata & { previousTransitionDurationMs?: number }, options?: { replaceExisting?: boolean, keepCamera?: boolean }) {
const context = MolstarLoadingContext.create();
const snapshot = loadTreeVirtual(plugin, tree, MolstarLoadingActions, context, options);
snapshot.canvas3d = {
props: plugin.canvas3d ? modifyCanvasProps(plugin.canvas3d.props, context.canvas) : undefined,
};
snapshot.camera = createPluginStateSnapshotCamera(plugin, context, metadata);
snapshot.durationInMs = metadata.linger_duration_ms + (metadata.previousTransitionDurationMs ?? 0);
const entryParams: PluginStateSnapshotManager.EntryParams = {
key: metadata.key,
name: metadata.title,
description: metadata.description,
descriptionFormat: metadata.description_format ?? 'markdown',
};
const entry: PluginStateSnapshotManager.Entry = PluginStateSnapshotManager.Entry(snapshot, entryParams);
return entry;
}
/** Mutable context for loading a `MolstarTree`, available throughout the loading. */
export interface MolstarLoadingContext {
/** Maps `*_from_[uri|source]` nodes to annotationId they should reference */
annotationMap?: Map<MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, string>,
annotationMap: Map<MolstarNode<AnnotationFromUriKind | AnnotationFromSourceKind>, string>,
/** Maps each node (on 'structure' or lower level) to its nearest 'representation' node */
nearestReprMap?: Map<MolstarNode, MolstarNode<'representation'>>,
focus?: { kind: 'camera', params: ParamsOfKind<MolstarTree, 'camera'> } | { kind: 'focus', focusTarget: StateObjectSelector, params: ParamsOfKind<MolstarTree, 'focus'> },
canvas?: ParamsOfKind<MolstarTree, 'canvas'>,
camera: {
cameraParams?: MolstarNodeParams<'camera'>,
focuses: { target: StateObjectSelector, params: MolstarNodeParams<'focus'> }[],
},
canvas?: MolstarNodeParams<'canvas'>,
}
export const MolstarLoadingContext = {
create(): MolstarLoadingContext {
return {
annotationMap: new Map(),
camera: { focuses: [] },
};
},
};
/** Loading actions for loading a `MolstarTree`, per node kind. */
@@ -135,7 +191,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
return undefined;
}
},
model(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'model'>, context: MolstarLoadingContext): UpdateTarget {
model(updateParent: UpdateTarget, node: MolstarSubtree<'model'>, context: MolstarLoadingContext): UpdateTarget {
const annotations = collectAnnotationReferences(node, context);
const model = UpdateTarget.apply(updateParent, ModelFromTrajectory, {
modelIndex: node.params.model_index,
@@ -152,7 +208,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
});
return model;
},
structure(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'structure'>, context: MolstarLoadingContext): UpdateTarget {
structure(updateParent: UpdateTarget, node: MolstarSubtree<'structure'>, context: MolstarLoadingContext): UpdateTarget {
const props = structureProps(node);
const struct = UpdateTarget.apply(updateParent, StructureFromModel, props);
let transformed = struct;
@@ -189,7 +245,7 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
tooltip: undefined, // No action needed, already loaded in `structure`
tooltip_from_uri: undefined, // No action needed, already loaded in `structure`
tooltip_from_source: undefined, // No action needed, already loaded in `structure`
component(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component'>): UpdateTarget | undefined {
component(updateParent: UpdateTarget, node: MolstarSubtree<'component'>): UpdateTarget | undefined {
if (isPhantomComponent(node)) {
return updateParent;
}
@@ -200,19 +256,19 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
nullIfEmpty: false,
});
},
component_from_uri(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component_from_uri'>, context: MolstarLoadingContext): UpdateTarget | undefined {
component_from_uri(updateParent: UpdateTarget, node: MolstarSubtree<'component_from_uri'>, context: MolstarLoadingContext): UpdateTarget | undefined {
if (isPhantomComponent(node)) return undefined;
const props = componentFromXProps(node, context);
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
},
component_from_source(updateParent: UpdateTarget, node: SubTreeOfKind<MolstarTree, 'component_from_source'>, context: MolstarLoadingContext): UpdateTarget | undefined {
component_from_source(updateParent: UpdateTarget, node: MolstarSubtree<'component_from_source'>, context: MolstarLoadingContext): UpdateTarget | undefined {
if (isPhantomComponent(node)) return undefined;
const props = componentFromXProps(node, context);
return UpdateTarget.apply(updateParent, MVSAnnotationStructureComponent, props);
},
representation(updateParent: UpdateTarget, node: MolstarNode<'representation'>, context: MolstarLoadingContext): UpdateTarget {
return UpdateTarget.apply(updateParent, StructureRepresentation3D, {
...representationProps(node.params),
...representationProps(node),
colorTheme: colorThemeForNode(node, context),
});
},
@@ -229,15 +285,40 @@ const MolstarLoadingActions: LoadingActions<MolstarTree, MolstarLoadingContext>
return UpdateTarget.apply(updateParent, StructureRepresentation3D, props);
},
focus(updateParent: UpdateTarget, node: MolstarNode<'focus'>, context: MolstarLoadingContext): UpdateTarget {
context.focus = { kind: 'focus', focusTarget: updateParent.selector, params: node.params };
context.camera.focuses.push({ target: updateParent.selector, params: node.params });
return updateParent;
},
camera(updateParent: UpdateTarget, node: MolstarNode<'camera'>, context: MolstarLoadingContext): UpdateTarget {
context.focus = { kind: 'camera', params: node.params };
context.camera.cameraParams = node.params;
return updateParent;
},
canvas(updateParent: UpdateTarget, node: MolstarNode<'canvas'>, context: MolstarLoadingContext): UpdateTarget {
context.canvas = node.params;
return updateParent;
},
primitives(updateParent: UpdateTarget, tree: MolstarSubtree<'primitives'>, context: MolstarLoadingContext): UpdateTarget {
const refs = getPrimitiveStructureRefs(tree);
const data = UpdateTarget.apply(updateParent, MVSInlinePrimitiveData, { node: tree });
return applyPrimitiveVisuals(data, refs);
},
primitives_from_uri(updateParent: UpdateTarget, tree: MolstarNode<'primitives_from_uri'>, context: MolstarLoadingContext): UpdateTarget {
const data = UpdateTarget.apply(updateParent, MVSDownloadPrimitiveData, { uri: tree.params.uri, format: tree.params.format });
return applyPrimitiveVisuals(data, new Set(tree.params.references));
},
};
function applyPrimitiveVisuals(data: UpdateTarget, refs: Set<string>) {
const mesh = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'mesh' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(mesh, ShapeRepresentation3D);
const labels = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'labels' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(labels, ShapeRepresentation3D);
const lines = UpdateTarget.setMvsDependencies(UpdateTarget.apply(data, MVSBuildPrimitiveShape, { kind: 'lines' }, { state: { isGhost: true } }), refs);
UpdateTarget.apply(lines, ShapeRepresentation3D);
return data;
}
export type MolstarLoadingExtension<TExtensionContext> = LoadingExtension<MolstarTree, MolstarLoadingContext, TExtensionContext>;
export const BuiltinLoadingExtensions: MolstarLoadingExtension<any>[] = [
NonCovalentInteractionsExtension,
];

View File

@@ -1,5 +1,5 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
@@ -9,27 +9,73 @@ import { treeToString } from './tree/generic/tree-utils';
import { Root, createMVSBuilder } from './tree/mvs/mvs-builder';
import { MVSTree, MVSTreeSchema } from './tree/mvs/mvs-tree';
/** Top level of the MolViewSpec (MVS) data format. */
export interface MVSData {
/** MolViewSpec tree */
root: MVSTree,
/** Associated metadata */
metadata: MVSMetadata,
}
interface MVSMetadata {
/** Version of the spec used to write this tree */
version: string,
/** Name of this view */
/** Top-level metadata for a MVS file (single-state or multi-state). */
export interface GlobalMetadata {
/** Name of this MVSData */
title?: string,
/** Detailed description of this view */
description?: string,
/** Format of the description */
/** Format of `description`. Default is 'markdown'. */
description_format?: 'markdown' | 'plaintext',
/** Timestamp when this view was exported */
/** Timestamp when this view was exported. */
timestamp: string,
/** Version of MolViewSpec used to write this file. */
version: string,
}
export const GlobalMetadata = {
create(metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): GlobalMetadata {
return {
...metadata,
version: `${MVSData.SupportedVersion}`,
timestamp: utcNowISO(),
};
},
};
/** Metadata for an individual snapshot. */
export interface SnapshotMetadata {
/** Name of this snapshot. */
title?: string,
/** Detailed description of this snapshot. */
description?: string,
/** Format of `description`. Default is 'markdown'. */
description_format?: 'markdown' | 'plaintext',
/** Unique identifier of this state, useful when working with collections of states. */
key?: string,
/** Timespan for snapshot. */
linger_duration_ms: number,
/** Timespan for the animation to the next snapshot. Leave empty to skip animations. */
transition_duration_ms?: number,
}
export interface Snapshot {
/** Root of the node tree */
root: MVSTree,
/** Associated metadata */
metadata: SnapshotMetadata,
}
/** MVSData with a single state */
export interface MVSData_State {
kind?: 'single',
/** Root of the node tree */
root: MVSTree,
/** Associated metadata */
metadata: GlobalMetadata,
}
/** MVSData with multiple states (snapshots) */
export interface MVSData_States {
kind: 'multiple',
/** Ordered collection of individual states */
snapshots: Snapshot[],
/** Associated metadata */
metadata: GlobalMetadata,
}
/** Top level of the MolViewSpec (MVS) data format. */
export type MVSData = MVSData_State | MVSData_States
export const MVSData = {
/** Currently supported major version of MolViewSpec format (e.g. 1 for version '1.0.8') */
@@ -62,15 +108,31 @@ export const MVSData = {
* If `options.noExtra` is true, presence of any extra node parameters is treated as an issue. */
validationIssues(mvsData: MVSData, options: { noExtra?: boolean } = {}): string[] | undefined {
const version = mvsData?.metadata?.version;
if (typeof version !== 'string') return [`"version" in MVS must be a string, not ${typeof version}: ${version}`];
if (mvsData.root === undefined) return [`"root" missing in MVS`];
return treeValidationIssues(MVSTreeSchema, mvsData.root, options);
if (typeof version !== 'string') return [`MVSData.metadata.version must be a string, not ${typeof version}: ${version}`];
if (mvsData.kind === 'single' || mvsData.kind === undefined) {
return snapshotValidationIssues(mvsData, options);
} else if (mvsData.kind === 'multiple') {
if (mvsData.snapshots === undefined) return [`"snapshots" missing in MVS`];
const issues: string[] = [];
for (const snapshot of mvsData.snapshots) { // would use .flatMap if it didn't work in a completely unpredictable way
const snapshotIssues = snapshotValidationIssues(snapshot, options);
if (snapshotIssues) issues.push(...snapshotIssues);
}
if (issues.length > 0) return issues;
else return undefined;
} else {
return [`MVSData.kind must be 'single' or 'multiple', not ${mvsData.kind}`];
}
},
/** Return a human-friendly textual representation of `mvsData`. */
toPrettyString(mvsData: MVSData): string {
const type = mvsData.kind === 'multiple' ? 'multiple states' : 'single state';
const title = mvsData.metadata.title !== undefined ? ` "${mvsData.metadata.title}"` : '';
return `MolViewSpec tree${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${treeToString(mvsData.root)}`;
const trees = mvsData.kind === 'multiple' ?
mvsData.snapshots.map((s, i) => `[Snapshot #${i}]\n${treeToString(s.root)}`).join('\n')
: treeToString(mvsData.root);
return `MolViewSpec ${type}${title} (version ${mvsData.metadata.version}, created ${mvsData.metadata.timestamp}):\n${trees}`;
},
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
@@ -86,6 +148,15 @@ export const MVSData = {
createBuilder(): Root {
return createMVSBuilder();
},
/** Create a multi-state MVS data from a list of snapshots. */
createMultistate(snapshots: Snapshot[], metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): MVSData_States {
return {
kind: 'multiple',
snapshots: [...snapshots],
metadata: GlobalMetadata.create(metadata),
};
},
};
@@ -96,3 +167,13 @@ function majorVersion(semanticVersion: string | number): number | undefined {
console.error(`Version should be a string, not ${typeof semanticVersion}: ${semanticVersion}`);
return undefined;
}
function snapshotValidationIssues(snapshot: MVSData_State | Snapshot, options: { noExtra?: boolean } = {}): string[] | undefined {
if (snapshot.root === undefined) return [`"root" missing in snapshot`];
return treeValidationIssues(MVSTreeSchema, snapshot.root, options);
}
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
function utcNowISO(): string {
return new Date().toISOString();
}

View File

@@ -0,0 +1,69 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { RequiredField, fieldValidationIssues, float, int, literal, nullable, str, union } from '../field-schema';
describe('fieldValidationIssues', () => {
it('fieldValidationIssues string', async () => {
const stringField = RequiredField(str, 'Testing required field stringField');
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
});
it('fieldValidationIssues string choice', async () => {
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'), 'Testing required field colorParam');
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues number choice', async () => {
const numberParam = RequiredField(literal(1, 2, 3, 4), 'Testing required field numberParam');
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues int', async () => {
const numberParam = RequiredField(int, 'Testing required field numberParam');
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues union', async () => {
const stringOrNumberParam = RequiredField(union([str, float]), 'Testing required field stringOrNumberParam');
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues nullable', async () => {
const stringOrNullParam = RequiredField(nullable(str), 'Testing required field stringOrNullParam');
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
});
});

View File

@@ -1,107 +1,85 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import * as iots from 'io-ts';
import { fieldValidationIssues, RequiredField, literal, nullable, paramsValidationIssues, OptionalField } from '../params-schema';
import { OptionalField, RequiredField, bool, float, int, str } from '../field-schema';
import { SimpleParamsSchema, UnionParamsSchema, paramsValidationIssues } from '../params-schema';
describe('fieldValidationIssues', () => {
it('fieldValidationIssues string', async () => {
const stringField = RequiredField(iots.string);
expect(fieldValidationIssues(stringField, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringField, '')).toBeUndefined();
expect(fieldValidationIssues(stringField, 5)).toBeTruthy();
expect(fieldValidationIssues(stringField, null)).toBeTruthy();
expect(fieldValidationIssues(stringField, undefined)).toBeTruthy();
});
it('fieldValidationIssues string choice', async () => {
const colorParam = RequiredField(literal('red', 'green', 'blue', 'yellow'));
expect(fieldValidationIssues(colorParam, 'red')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'green')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'blue')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'yellow')).toBeUndefined();
expect(fieldValidationIssues(colorParam, 'banana')).toBeTruthy();
expect(fieldValidationIssues(colorParam, 5)).toBeTruthy();
expect(fieldValidationIssues(colorParam, null)).toBeTruthy();
expect(fieldValidationIssues(colorParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues number choice', async () => {
const numberParam = RequiredField(literal(1, 2, 3, 4));
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 3)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 4)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues int', async () => {
const numberParam = RequiredField(iots.Integer);
expect(fieldValidationIssues(numberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0)).toBeUndefined();
expect(fieldValidationIssues(numberParam, 0.5)).toBeTruthy();
expect(fieldValidationIssues(numberParam, '1')).toBeTruthy();
expect(fieldValidationIssues(numberParam, null)).toBeTruthy();
expect(fieldValidationIssues(numberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues union', async () => {
const stringOrNumberParam = RequiredField(iots.union([iots.string, iots.number]));
expect(fieldValidationIssues(stringOrNumberParam, 1)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 2)).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNumberParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, null)).toBeTruthy();
expect(fieldValidationIssues(stringOrNumberParam, undefined)).toBeTruthy();
});
it('fieldValidationIssues nullable', async () => {
const stringOrNullParam = RequiredField(nullable(iots.string));
expect(fieldValidationIssues(stringOrNullParam, 'hello')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, '')).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, null)).toBeUndefined();
expect(fieldValidationIssues(stringOrNullParam, 1)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, true)).toBeTruthy();
expect(fieldValidationIssues(stringOrNullParam, undefined)).toBeTruthy();
});
const simpleSchema = SimpleParamsSchema({
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
surname: RequiredField(str, 'Testing optional field surname'),
lunch: RequiredField(bool, 'Testing optional field lunch'),
age: OptionalField(int, 0, 'Testing optional field age'),
});
const schema = {
name: OptionalField(iots.string),
surname: RequiredField(iots.string),
lunch: RequiredField(iots.boolean),
age: OptionalField(iots.number),
};
describe('validateParams', () => {
it('validateParams', async () => {
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, {}, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, {}, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
});
});
describe('validateFullParams', () => {
it('validateFullParams', async () => {
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(schema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(schema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29 }, { requireAll: true, noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(simpleSchema, {}, { requireAll: true, noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { requireAll: true, noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(simpleSchema, { name: 'John', surname: 'Doe', lunch: true, age: 29, married: true }, { requireAll: true, noExtra: false })).toBeUndefined(); // extra param `married`
});
});
const unionSchema = UnionParamsSchema(
'kind',
'Description for "kind"',
{
person: SimpleParamsSchema({
name: OptionalField(str, 'Anonymous', 'Testing optional field name'),
surname: RequiredField(str, 'Testing optional field surname'),
lunch: RequiredField(bool, 'Testing optional field lunch'),
age: OptionalField(int, 0, 'Testing optional field age'),
}),
object: SimpleParamsSchema({
weight: RequiredField(float, 'Testing optional field weight'),
color: OptionalField(str, 'colorless', 'Testing optional field color'),
}),
},
);
describe('validateUnionParams', () => {
it('validateUnionParams', async () => {
expect(paramsValidationIssues(unionSchema, { surname: 'Doe', lunch: true }, { noExtra: true })).toBeTruthy(); // missing discriminator param `kind`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 29 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'person' }, { noExtra: true })).toBeTruthy();
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', age: 29 }, { noExtra: true })).toBeTruthy(); // missing `lunch`
expect(paramsValidationIssues(unionSchema, { kind: 'person', name: 'John', surname: 'Doe', lunch: true, age: 'old' }, { noExtra: true })).toBeTruthy(); // wrong type of `age`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false }, { noExtra: true })).toBeTruthy(); // extra param `married`
expect(paramsValidationIssues(unionSchema, { kind: 'person', surname: 'Doe', lunch: true, married: false })).toBeUndefined(); // extra param `married`
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, color: 'black' }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42 }, { noExtra: true })).toBeUndefined();
expect(paramsValidationIssues(unionSchema, { kind: 'object', color: 'black' }, { noExtra: true })).toBeTruthy(); // missing param `weight`
expect(paramsValidationIssues(unionSchema, { kind: 'object', weight: 42, name: 'John' }, { noExtra: true })).toBeTruthy(); // extra param `name`
expect(paramsValidationIssues(unionSchema, { kind: 'spanish_inquisition' }, { noExtra: true })).toBeTruthy(); // unexpected value for discriminator param `kind`
});
});

View File

@@ -0,0 +1,107 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as iots from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { onelinerJsonString } from '../../../../mol-util/json';
/** All types that can be used in tree node params.
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {};
/** Type definition for a string */
export const str = iots.string;
/** Type definition for an integer */
export const int = iots.Integer;
/** Type definition for a float or integer number */
export const float = iots.number;
/** Type definition for a boolean */
export const bool = iots.boolean;
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
export const tuple = iots.tuple;
/** Type definition for a list/array, e.g. `list(str)` */
export const list = iots.array;
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
export const union = iots.union;
/** Type definition used to create objects */
export const obj = iots.type;
/** Type definition used to create partial objects */
export const partial = iots.partial;
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
export function nullable<T extends iots.Type<any>>(type: T) {
return union([type, iots.null]);
}
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
export function literal<V extends string | number | boolean>(...values: V[]) {
if (values.length === 0) {
throw new Error(`literal type must have at least one value`);
}
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
return new iots.Type<V>(
typeName,
((value: any) => values.includes(value)) as any,
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
value => value
);
}
/** Type definition for mapping between two types, e.g. `mapping(str, float)` means type `{ [key in string]: number }` */
export function mapping<A extends iots.Type<any>, B extends iots.Type<any>>(from: A, to: B) {
return iots.record(from, to);
}
interface FieldBase<V extends AllowedValueTypes = any, R extends boolean = boolean> {
/** Definition of allowed types for the field */
type: iots.Type<V>,
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
* If `required===false`, the value can be ommitted (meaning that a default should be used).
* If `type` allows `null`, the default must be `null`. */
required: R,
/** Description of what the field value means */
description: string,
}
/** Schema for param field which must always be provided (has no default value) */
export interface RequiredField<V extends AllowedValueTypes = any> extends FieldBase<V> {
required: true,
}
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description: string): RequiredField<V> {
return { type, required: true, description };
}
/** Schema for param field which can be dropped (meaning that a default value will be used) */
export interface OptionalField<V extends AllowedValueTypes = any> extends FieldBase<V> {
required: false,
/** Default value for optional field.
* If field type allows `null`, default must be `null` (this is to avoid issues in languages that do not distinguish `null` and `undefined`). */
default: DefaultValue<V>,
}
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, defaultValue: DefaultValue<V>, description: string): OptionalField<V> {
return { type, required: false, description, default: defaultValue };
}
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
export type Field<V extends AllowedValueTypes = any> = RequiredField<V> | OptionalField<V>;
/** Type of valid default value for value type `V` (if the type allows `null`, the default must be `null`) */
type DefaultValue<V extends AllowedValueTypes> = null extends V ? null : V;
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never;
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
* Return description of validation issues, if `value` has wrong type. */
export function fieldValidationIssues<F extends Field>(field: F, value: any): string[] | undefined {
const validation = field.type.decode(value);
if (validation._tag === 'Right') {
return undefined;
} else {
return PathReporter.report(validation);
}
}

View File

@@ -1,140 +1,179 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as iots from 'io-ts';
import { PathReporter } from 'io-ts/PathReporter';
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
import { onelinerJsonString } from '../../../../mol-util/json';
import { isPlainObject, mapObjectMap, omitObjectKeys } from '../../../../mol-util/object';
import { Field, fieldValidationIssues, OptionalField, RequiredField, ValueFor } from './field-schema';
/** All types that can be used in tree node params.
* Can be extended, this is just to list them all in one place and possibly catch some typing errors */
type AllowedValueTypes = string | number | boolean | null | [number, number, number] | string[] | number[] | {}
type Fields = { [key in string]: Field };
/** Type definition for a string */
export const str = iots.string;
/** Type definition for an integer */
export const int = iots.Integer;
/** Type definition for a float or integer number */
export const float = iots.number;
/** Type definition for a boolean */
export const bool = iots.boolean;
/** Type definition for a tuple, e.g. `tuple([str, int, int])` */
export const tuple = iots.tuple;
/** Type definition for a list/array, e.g. `list(str)` */
export const list = iots.array;
/** Type definition for union types, e.g. `union([str, int])` means string or integer */
export const union = iots.union;
/** Type definition for nullable types, e.g. `nullable(str)` means string or `null` */
export function nullable<T extends iots.Type<any>>(type: T) {
return union([type, iots.null]);
/** Type of `ParamsSchema` where all fields are completely independent */
export interface SimpleParamsSchema<TFields extends Fields = Fields> {
type: 'simple',
/** Parameter fields */
fields: TFields,
}
/** Type definition for literal types, e.g. `literal('red', 'green', 'blue')` means 'red' or 'green' or 'blue' */
export function literal<V extends string | number | boolean>(...values: V[]) {
if (values.length === 0) {
throw new Error(`literal type must have at least one value`);
}
const typeName = `(${values.map(v => onelinerJsonString(v)).join(' | ')})`;
return new iots.Type<V>(
typeName,
((value: any) => values.includes(value)) as any,
(value, ctx) => values.includes(value as any) ? { _tag: 'Right', right: value as any } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid value for literal type ${typeName}` }] },
value => value
);
export function SimpleParamsSchema<TFields extends Fields>(fields: TFields): SimpleParamsSchema<TFields> {
return { type: 'simple', fields };
}
type ValuesForFields<F extends Fields> =
{ [key in keyof F as (F[key] extends RequiredField<any> ? key : never)]: ValueFor<F[key]> }
& { [key in keyof F as (F[key] extends OptionalField<any> ? key : never)]?: ValueFor<F[key]> };
/** Schema for one field in params (i.e. a value in a top-level key-value pair) */
interface Field<V extends AllowedValueTypes = any, R extends boolean = boolean> {
/** Definition of allowed types for the field */
type: iots.Type<V>,
/** If `required===true`, the value must always be defined in molviewspec format (can be `null` if `type` allows it).
* If `required===false`, the value can be ommitted (meaning that a default should be used).
* If `type` allows `null`, the default must be `null`. */
required: R,
/** Description of what the field value means */
description?: string,
type ValuesForSimpleParamsSchema<TSchema extends SimpleParamsSchema> = ValuesForFields<TSchema['fields']>;
type AllRequiredFields<F extends Fields>
= { [key in keyof F]: F[key] extends Field<infer V> ? RequiredField<V> : never };
type AllRequiredSimple<TSchema extends SimpleParamsSchema> = SimpleParamsSchema<AllRequiredFields<TSchema['fields']>>;
type Cases = { [case_ in string]: SimpleParamsSchema };
// Tried to have this recursive ({ [case_ in string]: ParamsSchema }) but ran into "ts(2589) Type instantiation is excessively deep..."
/** Type of `ParamsSchema` where one field (discriminator) determines what other fields are allowed (i.e. discriminated union type) */
export interface UnionParamsSchema<TDiscriminator extends string = string, TCases extends Cases = Cases> {
type: 'union',
/** Name of parameter field that determines the rest (allowed values are defined by keys of `cases`) */
discriminator: TDiscriminator,
/** Description for the discriminator parameter field */
discriminatorDescription: string,
/** `ParamsSchema` for the rest, for each case of discriminator value */
cases: TCases,
}
/** Schema for param field which must always be provided (has no default value) */
export interface RequiredField<V extends AllowedValueTypes = any> extends Field<V> {
required: true,
}
export function RequiredField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): RequiredField<V> {
return { type, required: true, description };
export function UnionParamsSchema<TDiscriminator extends string, TCases extends Cases>(discriminator: TDiscriminator, discriminatorDescription: string, cases: TCases): UnionParamsSchema<TDiscriminator, TCases> {
return { type: 'union', discriminator, discriminatorDescription, cases };
}
/** Schema for param field which can be dropped (meaning that a default value will be used) */
export interface OptionalField<V extends AllowedValueTypes = any> extends Field<V> {
required: false,
}
export function OptionalField<V extends AllowedValueTypes>(type: iots.Type<V>, description?: string): OptionalField<V> {
return { type, required: false, description };
}
type ValuesForUnionParamsSchema<TSchema extends UnionParamsSchema, TCase extends keyof TSchema['cases'] = keyof TSchema['cases']>
= TCase extends keyof TSchema['cases'] ? { [discriminator in TSchema['discriminator']]: TCase } & ValuesFor<TSchema['cases'][TCase]> : never;
// `extends` clause seems superfluous here, but is needed to properly create discriminated union type
/** Type of valid value for field of type `F` (never includes `undefined`, even if field is optional) */
export type ValueFor<F extends Field | iots.Any> = F extends Field<infer V> ? V : F extends iots.Any ? iots.TypeOf<F> : never
/** Type of valid default value for field of type `F` (if the field's type allows `null`, the default must be `null`) */
export type DefaultFor<F extends Field> = F extends Field<infer V> ? (null extends V ? null : V) : never
/** Return `undefined` if `value` has correct type for `field`, regardsless of if required or optional.
* Return description of validation issues, if `value` has wrong type. */
export function fieldValidationIssues<F extends Field, V>(field: F, value: V): V extends ValueFor<F> ? undefined : string[] {
const validation = field.type.decode(value);
if (validation._tag === 'Right') {
return undefined as any;
} else {
return PathReporter.report(validation) as any;
}
}
type AllRequiredUnion<TSchema extends UnionParamsSchema>
= UnionParamsSchema<TSchema['discriminator'], { [case_ in keyof TSchema['cases']]: AllRequired<TSchema['cases'][case_]> }>;
/** Schema for "params", i.e. a flat collection of key-value pairs */
export type ParamsSchema<TKey extends string = string> = { [key in TKey]: Field }
/** Variation of a params schema where all fields are required */
export type AllRequired<TParamsSchema extends ParamsSchema> = { [key in keyof TParamsSchema]: TParamsSchema[key] extends Field<infer V> ? RequiredField<V> : never }
export function AllRequired<TParamsSchema extends ParamsSchema>(paramsSchema: TParamsSchema): AllRequired<TParamsSchema> {
return mapObjectMap(paramsSchema, field => RequiredField(field.type, field.description)) as AllRequired<TParamsSchema>;
}
export type ParamsSchema = SimpleParamsSchema | UnionParamsSchema;
/** Type of values for a params schema (optional fields can be missing) */
export type ValuesFor<P extends ParamsSchema> =
{ [key in keyof P as (P[key] extends RequiredField<any> ? key : never)]: ValueFor<P[key]> }
& { [key in keyof P as (P[key] extends OptionalField<any> ? key : never)]?: ValueFor<P[key]> }
export type ValuesFor<P extends ParamsSchema>
= P extends SimpleParamsSchema ? ValuesForSimpleParamsSchema<P> : P extends UnionParamsSchema ? ValuesForUnionParamsSchema<P> : never;
/** Variation of a params schema where all fields are required */
export type AllRequired<P extends ParamsSchema>
= P extends SimpleParamsSchema ? AllRequiredSimple<P> : P extends UnionParamsSchema ? AllRequiredUnion<P> : never;
function AllRequiredSimple<TSchema extends SimpleParamsSchema>(schema: TSchema): AllRequired<TSchema> {
const newFields = mapObjectMap(schema.fields, field => RequiredField(field.type, field.description));
return SimpleParamsSchema(newFields) as AllRequired<TSchema>;
}
function AllRequiredUnion<TSchema extends UnionParamsSchema>(schema: TSchema): AllRequired<TSchema> {
const newCases = mapObjectMap(schema.cases, AllRequired);
return UnionParamsSchema(schema.discriminator, schema.discriminatorDescription, newCases) as AllRequired<TSchema>;
}
export function AllRequired<TSchema extends ParamsSchema>(schema: TSchema): AllRequired<TSchema> {
if (schema.type === 'simple') {
return AllRequiredSimple(schema) as AllRequired<TSchema>;
} else {
return AllRequiredUnion(schema) as AllRequired<TSchema>;
}
}
/** Type of full values for a params schema, i.e. including all optional fields */
export type FullValuesFor<P extends ParamsSchema> = { [key in keyof P]: ValueFor<P[key]> }
export type FullValuesFor<P extends ParamsSchema> = ValuesFor<AllRequired<P>>;
/** Type of default values for a params schema, i.e. including only optional fields */
export type DefaultsFor<P extends ParamsSchema> = { [key in keyof P as (P[key] extends Field<any, false> ? key : never)]: ValueFor<P[key]> }
interface ValidationOptions {
/** Check that all parameters (including optional) have a value provided. */
requireAll?: boolean,
/** Check there are extra parameters other that those defined in the schema. */
noExtra?: boolean,
}
/** Return `undefined` if `values` contains correct value types for `schema`,
* return description of validation issues, if `values` have wrong type.
* If `options.requireAll`, all parameters (including optional) must have a value provided.
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue.
*/
export function paramsValidationIssues<P extends ParamsSchema, V extends { [k: string]: any }>(schema: P, values: V, options: { requireAll?: boolean, noExtra?: boolean } = {}): string[] | undefined {
* If `options.noExtra` is true, presence of any extra parameters is treated as an issue. */
export function paramsValidationIssues<P extends ParamsSchema>(schema: P, values: { [k: string]: any }, options: ValidationOptions = {}): string[] | undefined {
if (!isPlainObject(values)) return [`Parameters must be an object, not ${values}`];
for (const key in schema) {
const paramDef = schema[key];
if (schema.type === 'simple') {
return simpleParamsValidationIssue(schema, values, options);
} else {
return unionParamsValidationIssues(schema, values, options);
}
}
function simpleParamsValidationIssue(schema: SimpleParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
for (const key in schema.fields) {
const fieldSchema = schema.fields[key];
if (Object.hasOwn(values, key)) {
const value = values[key];
const issues = fieldValidationIssues(paramDef, value);
if (issues) return [`Invalid type for parameter "${key}":`, ...issues.map(s => ' ' + s)];
const issues = fieldValidationIssues(fieldSchema, value);
if (issues) return [`Invalid value for parameter "${key}":`, ...issues.map(s => ' ' + s)];
} else {
if (paramDef.required) return [`Missing required parameter "${key}".`];
if (fieldSchema.required) return [`Missing required parameter "${key}".`];
if (options.requireAll) return [`Missing optional parameter "${key}".`];
}
}
if (options.noExtra) {
for (const key in values) {
if (!Object.hasOwn(schema, key)) return [`Unknown parameter "${key}".`];
if (!Object.hasOwn(schema.fields, key)) return [`Unknown parameter "${key}".`];
}
}
return undefined;
}
function unionParamsValidationIssues(schema: UnionParamsSchema, values: { [k: string]: any }, options: ValidationOptions): string[] | undefined {
if (!Object.hasOwn(values, schema.discriminator)) {
return [`Missing required parameter "${schema.discriminator}".`];
}
const case_ = values[schema.discriminator];
const subschema = schema.cases[case_];
if (subschema === undefined) {
const allowedCases = Object.keys(schema.cases).map(x => `"${x}"`).join(' | ');
return [
`Invalid value for parameter "${schema.discriminator}":`,
`"${case_}" is not a valid value for literal type (${allowedCases})`,
];
}
const issues = paramsValidationIssues(subschema, omitObjectKeys(values, [schema.discriminator]), options);
if (issues) {
issues.unshift(`(case "${schema.discriminator}": "${case_}")`);
return issues.map(s => ' ' + s);
}
return undefined;
}
/** Add default parameter values to `values` based on a parameter schema (only for optional parameters) */
export function addParamDefaults<P extends ParamsSchema>(schema: P, values: ValuesFor<P>): FullValuesFor<P> {
if (schema.type === 'simple') {
return addSimpleParamsDefaults(schema, values);
} else {
return addUnionParamsDefaults(schema, values);
}
}
function addSimpleParamsDefaults(schema: SimpleParamsSchema, values: any): any {
const out = { ...values };
for (const key in schema.fields) {
const field = schema.fields[key];
if (!field.required && out[key] === undefined) {
out[key] = field.default;
}
}
return out;
}
function addUnionParamsDefaults(schema: UnionParamsSchema, values: any): any {
const case_ = values[schema.discriminator];
const subschema = schema.cases[case_];
return addParamDefaults(subschema, values);
}

View File

@@ -1,24 +1,32 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { onelinerJsonString } from '../../../../mol-util/json';
import { isPlainObject, mapObjectMap } from '../../../../mol-util/object';
import { AllRequired, DefaultsFor, ParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
import { Field } from './field-schema';
import { AllRequired, ParamsSchema, SimpleParamsSchema, ValuesFor, paramsValidationIssues } from './params-schema';
import { treeToString } from './tree-utils';
/** Type of "custom" of a tree node (key-value storage with arbitrary JSONable values) */
export type CustomProps = Partial<Record<string, any>>
/** Tree node without children */
export type Node<TKind extends string = string, TParams extends {} = {}> =
{} extends TParams ? {
kind: TKind,
params?: TParams,
params?: TParams, // params can be dropped if {} is valid value for params
custom?: CustomProps,
ref?: string,
} : {
kind: TKind,
params: TParams,
} // params can be dropped if {} is valid value for params
params: TParams, // params must be here if {} is not valid value for params
custom?: CustomProps,
ref?: string,
}
/** Kind type for a tree node */
export type Kind<TNode extends Node> = TNode['kind']
@@ -34,23 +42,27 @@ export type Tree<TNode extends Node<string, {}> = Node<string, {}>, TRoot extend
}
/** Type of any subtree that can occur within given `TTree` tree type */
export type SubTree<TTree extends Tree> = NonNullable<TTree['children']>[number]
export type Subtree<TTree extends Tree> = NonNullable<TTree['children']>[number]
/** Type of any subtree that can occur within given `TTree` tree type and has kind type `TKind` */
export type SubTreeOfKind<TTree extends Tree, TKind extends Kind<SubTree<TTree>> = Kind<SubTree<TTree>>> = RootOfKind<SubTree<TTree>, TKind>
export type SubtreeOfKind<TTree extends Tree, TKind extends Kind<Subtree<TTree>> = Kind<Subtree<TTree>>> = RootOfKind<Subtree<TTree>, TKind>
type RootOfKind<TTree extends Tree, TKind extends Kind<TTree>> = Extract<TTree, Tree<any, Node<TKind>>>
/** Params type for a given kind type within a tree */
export type ParamsOfKind<TTree extends Tree, TKind extends Kind<SubTree<TTree>> = Kind<SubTree<TTree>>> = NonNullable<SubTreeOfKind<TTree, TKind>['params']>
export type ParamsOfKind<TTree extends Tree, TKind extends Kind<Subtree<TTree>> = Kind<Subtree<TTree>>> = NonNullable<SubtreeOfKind<TTree, TKind>['params']>
/** Get params from a tree node */
export function getParams<TNode extends Node>(node: TNode): Params<TNode> {
return node.params ?? {};
}
/** Get custom properties from a tree node */
export function getCustomProps<TCustomProps extends CustomProps = CustomProps>(node: Node): TCustomProps {
return (node.custom ?? {}) as TCustomProps;
}
/** Get children from a tree node */
export function getChildren<TTree extends Tree>(tree: TTree): SubTree<TTree>[] {
export function getChildren<TTree extends Tree>(tree: TTree): Subtree<TTree>[] {
return tree.children ?? [];
}
@@ -74,7 +86,7 @@ export interface TreeSchema<TParamsSchemas extends ParamsSchemas = ParamsSchemas
},
}
export function TreeSchema<P extends ParamsSchemas = ParamsSchemas, R extends keyof P = string>(schema: TreeSchema<P, R>): TreeSchema<P, R> {
return schema as any;
return schema;
}
/** ParamsSchemas per node kind */
@@ -103,9 +115,6 @@ export type NodeFor<TTreeSchema extends TreeSchema, TKind extends keyof ParamsSc
/** Type of tree which conforms to tree schema `TTreeSchema` */
export type TreeFor<TTreeSchema extends TreeSchema> = Tree<NodeFor<TTreeSchema>, RootFor<TTreeSchema> & NodeFor<TTreeSchema>>
/** Type of default parameter values for each node kind in a tree schema `TTreeSchema` */
export type DefaultsForTree<TTreeSchema extends TreeSchema> = { [kind in keyof TTreeSchema['nodes']]: DefaultsFor<TTreeSchema['nodes'][kind]['params']> }
/** Return `undefined` if a tree conforms to the given schema,
* return validation issues (as a list of lines) if it does not conform.
@@ -123,6 +132,9 @@ export function treeValidationIssues(schema: TreeSchema, tree: Tree, options: {
}
const issues = paramsValidationIssues(nodeSchema.params, getParams(tree), options);
if (issues) return [`Invalid parameters for node of kind "${tree.kind}":`, ...issues.map(s => ' ' + s)];
if (tree.custom !== undefined && (typeof tree.custom !== 'object' || tree.custom === null)) {
return [`Invalid "custom" for node of kind "${tree.kind}": must be an object, not ${tree.custom}.`];
}
for (const child of getChildren(tree)) {
const issues = treeValidationIssues(schema, child, { ...options, anyRoot: true, parent: tree.kind });
if (issues) return issues;
@@ -146,14 +158,14 @@ export function validateTree(schema: TreeSchema, tree: Tree, label: string): voi
}
/** Return documentation for a tree schema as plain text */
export function treeSchemaToString<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
return treeSchemaToString_(schema, defaults, false);
export function treeSchemaToString<S extends TreeSchema>(schema: S): string {
return treeSchemaToString_(schema, false);
}
/** Return documentation for a tree schema as markdown text */
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>): string {
return treeSchemaToString_(schema, defaults, true);
export function treeSchemaToMarkdown<S extends TreeSchema>(schema: S): string {
return treeSchemaToString_(schema, true);
}
function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: DefaultsForTree<S>, markdown: boolean = false): string {
function treeSchemaToString_<S extends TreeSchema>(schema: S, markdown: boolean = false): string {
const out: string[] = [];
const bold = (str: string) => markdown ? `**${str}**` : str;
const code = (str: string) => markdown ? `\`${str}\`` : str;
@@ -161,6 +173,8 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: Default
const p1 = markdown ? '' : ' ';
const h2 = markdown ? '- ' : ' - ';
const p2 = markdown ? ' ' : ' ';
const h3 = markdown ? ' - ' : ' - ';
const p3 = markdown ? ' ' : ' ';
const newline = markdown ? '\n\n' : '\n';
out.push(`Tree schema:`);
for (const kind in schema.nodes) {
@@ -174,21 +188,46 @@ function treeSchemaToString_<S extends TreeSchema>(schema: S, defaults?: Default
}
out.push(`${p1}Parent: ${!parent ? 'any' : parent.length === 0 ? 'none' : parent.map(code).join(' or ')}`);
out.push(`${p1}Params:${Object.keys(params).length > 0 ? '' : ' none'}`);
for (const key in params) {
const field = params[key];
let typeString = field.type.name;
if (typeString.startsWith('(') && typeString.endsWith(')')) {
typeString = typeString.slice(1, -1);
if (params.type === 'simple') {
formatSimpleParams(out, params, { h: h2, p: p2, code, bold });
} else {
const key = params.discriminator;
const casesStr = Object.keys(params.cases).join(' | ');
out.push(`${h2}${bold(code(key + ': '))}${code(casesStr)}`);
if (params.discriminatorDescription) {
out.push(`${p2}${params.discriminatorDescription}`);
}
out.push(`${h2}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(typeString)}`);
const defaultValue = (defaults?.[kind] as any)?.[key];
if (field.description) {
out.push(`${p2}${field.description}`);
}
if (defaultValue !== undefined) {
out.push(`${p2}Default: ${code(onelinerJsonString(defaultValue))}`);
out.push(`${p2}[This parameter determines the rest of parameters]`);
for (const case_ in params.cases) {
const caseStr = `${params.discriminator}: "${case_}"`;
out.push(`${p2}${bold(`Case ${code(caseStr)}:`)}`);
formatSimpleParams(out, params.cases[case_], { h: h3, p: p3, code, bold });
}
}
}
return out.join(newline);
}
function formatSimpleParams(out: string[], params: SimpleParamsSchema, formatting: { h: string, p: string, code: (str: string) => string, bold: (str: string) => string }): void {
const { h, p, code, bold } = formatting;
for (const key in params.fields) {
const field = params.fields[key];
out.push(`${h}${bold(code(key + (field.required ? ': ' : '?: ')))}${code(formatFieldType(field))}`);
const defaultValue = field.required ? undefined : field.default;
if (field.description) {
out.push(`${p}${field.description}`);
}
if (defaultValue !== undefined) {
out.push(`${p}Default: ${code(onelinerJsonString(defaultValue))}`);
}
}
}
function formatFieldType(field: Field): string {
const typeString = field.type.name;
if (typeString.startsWith('(') && typeString.endsWith(')')) {
return typeString.slice(1, -1);
} else {
return typeString;
}
}

View File

@@ -1,23 +1,24 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { canonicalJsonString } from '../../../../mol-util/json';
import { DefaultsForTree, Kind, SubTree, SubTreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
import { addParamDefaults } from './params-schema';
import { CustomProps, Kind, Node, Subtree, SubtreeOfKind, Tree, TreeFor, TreeSchema, TreeSchemaWithAllRequired, getParams } from './tree-schema';
/** Run DFS (depth-first search) algorithm on a rooted tree.
* Runs `visit` function when a node is discovered (before visiting any descendants).
* Runs `postVisit` function when leaving a node (after all descendants have been visited). */
export function dfs<TTree extends Tree>(root: TTree, visit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any, postVisit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any) {
return _dfs<SubTree<TTree>>(root, undefined, visit, postVisit);
export function dfs<TTree extends Tree>(root: TTree, visit?: (node: Subtree<TTree>, parent?: Subtree<TTree>) => any, postVisit?: (node: Subtree<TTree>, parent?: Subtree<TTree>) => any) {
return _dfs<Subtree<TTree>>(root, undefined, visit, postVisit);
}
function _dfs<TTree extends Tree>(root: TTree, parent: SubTree<TTree> | undefined, visit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any, postVisit?: (node: SubTree<TTree>, parent?: SubTree<TTree>) => any) {
function _dfs<TTree extends Tree>(root: TTree, parent: Subtree<TTree> | undefined, visit?: (node: Subtree<TTree>, parent?: Subtree<TTree>) => any, postVisit?: (node: Subtree<TTree>, parent?: Subtree<TTree>) => any) {
if (visit) visit(root, parent);
for (const child of root.children ?? []) {
_dfs<SubTree<TTree>>(child, root, visit, postVisit);
_dfs<Subtree<TTree>>(child, root, visit, postVisit);
}
if (postVisit) postVisit(root, parent);
}
@@ -26,9 +27,12 @@ function _dfs<TTree extends Tree>(root: TTree, parent: SubTree<TTree> | undefine
export function treeToString(tree: Tree) {
let level = 0;
const lines: string[] = [];
dfs(tree, node => lines.push(' '.repeat(level++) + `- ${node.kind} ${formatObject(node.params ?? {})}`), node => level--);
dfs(tree, node => lines.push(' '.repeat(level++) + nodeToString(node)), node => level--);
return lines.join('\n');
}
function nodeToString(node: Node) {
return `- ${node.kind} ${formatObject(node.params ?? {})}${formatCustomProps(node.custom)}${formatRef(node.ref)}`;
}
/** Convert object to a human-friendly string (similar to JSON.stringify but without quoting keys) */
export function formatObject(obj: {} | undefined): string {
@@ -36,12 +40,26 @@ export function formatObject(obj: {} | undefined): string {
return JSON.stringify(obj).replace(/,("\w+":)/g, ', $1').replace(/"(\w+)":/g, '$1: ');
}
/** Return human-friendly string with node custom properties, if any */
function formatCustomProps(customProps: CustomProps | undefined): string {
if (!customProps || Object.keys(customProps).length === 0) return '';
return `, custom: ${formatObject(customProps)}`;
}
/** Return human-friendly string with node ref, if any */
function formatRef(ref: string | undefined): string {
if (ref === undefined) return '';
return `, ref: "${ref}"`;
}
/** Create a copy of a tree node, ignoring children. */
export function copyNodeWithoutChildren<TTree extends Tree>(node: TTree): TTree {
return {
kind: node.kind,
params: node.params ? { ...node.params } : undefined,
custom: node.custom ? { ...node.custom } : undefined,
ref: node.ref,
} as TTree;
}
/** Create a copy of a tree node, including a shallow copy of children. */
@@ -49,6 +67,8 @@ export function copyNode<TTree extends Tree>(node: TTree): TTree {
return {
kind: node.kind,
params: node.params ? { ...node.params } : undefined,
custom: node.custom ? { ...node.custom } : undefined,
ref: node.ref,
children: node.children ? [...node.children] : undefined,
} as TTree;
}
@@ -66,15 +86,15 @@ export function copyTree<T extends Tree>(root: T): T {
* nodes of kind `C` will be converted to `Y` with a child `Z` (original children moved to `Z`),
* nodes of other kinds will just be copied. */
export type ConversionRules<A extends Tree, B extends Tree> = {
[kind in Kind<SubTree<A>>]?: (node: SubTreeOfKind<A, kind>, parent?: SubTree<A>) => SubTree<B>[]
[kind in Kind<Subtree<A>>]?: (node: SubtreeOfKind<A, kind>, parent?: Subtree<A>) => Subtree<B>[]
};
/** Apply a set of conversion rules to a tree to change to a different schema. */
export function convertTree<A extends Tree, B extends Tree>(root: A, conversions: ConversionRules<A, B>): SubTree<B> {
const mapping = new Map<SubTree<A>, SubTree<B>>();
let convertedRoot: SubTree<B>;
export function convertTree<A extends Tree, B extends Tree>(root: A, conversions: ConversionRules<A, B>): Subtree<B> {
const mapping = new Map<Subtree<A>, Subtree<B>>();
let convertedRoot: Subtree<B>;
dfs<A>(root, (node, parent) => {
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: SubTree<A>) => SubTree<B>[]) | undefined;
const conversion = conversions[node.kind as (typeof node)['kind']] as ((n: typeof node, p?: Subtree<A>) => Subtree<B>[]) | undefined;
if (conversion) {
const convertidos = conversion(node, parent);
if (!parent && convertidos.length === 0) throw new Error('Cannot convert root to empty path');
@@ -104,13 +124,13 @@ export function convertTree<A extends Tree, B extends Tree>(root: A, conversions
/** Create a copy of the tree where twins (siblings of the same kind with the same params) are merged into one node.
* Applies only to the node kinds listed in `condenseNodes` (or all if undefined) except node kinds in `skipNodes`. */
export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<Tree>>, skipNodes?: Set<Kind<Tree>>): T {
const map = new Map<string, SubTree<T>>();
const map = new Map<string, Subtree<T>>();
const result = copyTree(root);
dfs<T>(result, node => {
map.clear();
const newChildren: SubTree<T>[] = [];
const newChildren: Subtree<T>[] = [];
for (const child of node.children ?? []) {
let twin: SubTree<T> | undefined = undefined;
let twin: Subtree<T> | undefined = undefined;
const doApply = (!condenseNodes || condenseNodes.has(child.kind)) && !skipNodes?.has(child.kind);
if (doApply) {
const key = child.kind + canonicalJsonString(getParams(child));
@@ -120,7 +140,7 @@ export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<T
if (twin) {
(twin.children ??= []).push(...child.children ?? []);
} else {
newChildren.push(child as SubTree<T>);
newChildren.push(child as Subtree<T>);
}
}
node.children = newChildren;
@@ -129,10 +149,16 @@ export function condenseTree<T extends Tree>(root: T, condenseNodes?: Set<Kind<T
}
/** Create a copy of the tree where missing optional params for each node are added based on `defaults`. */
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, defaults: DefaultsForTree<S>): TreeFor<TreeSchemaWithAllRequired<S>> {
const rules: ConversionRules<TreeFor<S>, TreeFor<S>> = {};
for (const kind in defaults) {
rules[kind] = node => [{ kind: node.kind, params: { ...defaults[kind], ...node.params } } as any];
export function addDefaults<S extends TreeSchema>(tree: TreeFor<S>, treeSchema: S): TreeFor<TreeSchemaWithAllRequired<S>> {
type TTree = TreeFor<S>;
const rules: ConversionRules<TTree, TTree> = {};
for (const kind in treeSchema.nodes) {
rules[kind as Kind<Subtree<TTree>>] = node => [{
kind: node.kind,
params: addParamDefaults(treeSchema.nodes[kind].params, node.params as any),
custom: node.custom,
ref: node.ref,
} as Node as any];
}
return convertTree(tree, rules) as any;
}

View File

@@ -1,15 +1,14 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
import { MVSDefaults } from '../mvs/mvs-defaults';
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
import { ConversionRules, addDefaults, condenseTree, convertTree, dfs, resolveUris } from '../generic/tree-utils';
import { FullMVSTree, MVSTree, MVSTreeSchema } from '../mvs/mvs-tree';
import { MolstarParseFormatT, ParseFormatT } from '../mvs/param-types';
import { MolstarKind, MolstarNode, MolstarTree } from './molstar-tree';
/** Convert `format` parameter of `parse` node in `MolstarTree`
@@ -26,10 +25,10 @@ const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
'download': node => [],
'parse': (node, parent) => {
const { format, is_binary } = ParseFormatMvsToMolstar[node.params.format];
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format } };
const convertedNode: MolstarNode<'parse'> = { kind: 'parse', params: { ...node.params, format }, custom: node.custom, ref: node.ref };
if (parent?.kind === 'download') {
return [
{ kind: 'download', params: { ...parent.params, is_binary } },
{ kind: 'download', params: { ...parent.params, is_binary }, custom: parent.custom, ref: parent.ref },
convertedNode,
] satisfies MolstarNode[];
} else {
@@ -38,12 +37,12 @@ const mvsToMolstarConversionRules: ConversionRules<FullMVSTree, MolstarTree> = {
}
},
'structure': (node, parent) => {
if (parent?.kind !== 'parse') throw new Error('Parent of "structure" must be "parse".');
if (parent?.kind !== 'parse') throw new Error(`Parent of "structure" must be "parse", not "${parent?.kind}".`);
const { format } = ParseFormatMvsToMolstar[parent.params.format];
return [
{ kind: 'trajectory', params: { format, ...pickObjectKeys(node.params, ['block_header', 'block_index']) } },
{ kind: 'model', params: pickObjectKeys(node.params, ['model_index']) },
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']) },
{ kind: 'structure', params: omitObjectKeys(node.params, ['block_header', 'block_index', 'model_index']), custom: node.custom, ref: node.ref },
] satisfies MolstarNode[];
},
};
@@ -53,7 +52,7 @@ const molstarNodesToCondense = new Set<MolstarKind>(['download', 'parse', 'traje
/** Convert MolViewSpec tree into MolStar tree */
export function convertMvsToMolstar(mvsTree: MVSTree, sourceUrl: string | undefined): MolstarTree {
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSDefaults) as FullMVSTree;
const full = addDefaults<typeof MVSTreeSchema>(mvsTree, MVSTreeSchema) as FullMVSTree;
if (sourceUrl) resolveUris(full, sourceUrl, ['uri', 'url']);
const converted = convertTree<FullMVSTree, MolstarTree>(full, mvsToMolstarConversionRules);
if (converted.kind !== 'root') throw new Error("Root's type is not 'root' after conversion from MVS tree to Molstar tree.");

View File

@@ -1,12 +1,13 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { omitObjectKeys, pickObjectKeys } from '../../../../mol-util/object';
import { RequiredField, bool } from '../generic/params-schema';
import { NodeFor, TreeFor, TreeSchema } from '../generic/tree-schema';
import { RequiredField, bool } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema } from '../generic/tree-schema';
import { FullMVSTreeSchema } from '../mvs/mvs-tree';
import { MolstarParseFormatT } from '../mvs/param-types';
@@ -18,37 +19,41 @@ export const MolstarTreeSchema = TreeSchema({
...FullMVSTreeSchema.nodes,
download: {
...FullMVSTreeSchema.nodes.download,
params: {
...FullMVSTreeSchema.nodes.download.params,
is_binary: RequiredField(bool),
},
params: SimpleParamsSchema({
...FullMVSTreeSchema.nodes.download.params.fields,
is_binary: RequiredField(bool, 'Specifies whether file is downloaded as bytes array or string'),
}),
},
parse: {
...FullMVSTreeSchema.nodes.parse,
params: {
format: RequiredField(MolstarParseFormatT),
},
params: SimpleParamsSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
}),
},
/** Auxiliary node corresponding to Molstar's TrajectoryFrom*. */
trajectory: {
description: "Auxiliary node corresponding to Molstar's TrajectoryFrom*.",
parent: ['parse'],
params: {
format: RequiredField(MolstarParseFormatT),
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index'] as const),
},
params: SimpleParamsSchema({
format: RequiredField(MolstarParseFormatT, 'File format'),
...pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index'] as const),
}),
},
/** Auxiliary node corresponding to Molstar's ModelFromTrajectory. */
model: {
description: "Auxiliary node corresponding to Molstar's ModelFromTrajectory.",
parent: ['trajectory'],
params: pickObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['model_index'] as const),
params: SimpleParamsSchema(
pickObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['model_index'] as const)
),
},
/** Auxiliary node corresponding to Molstar's StructureFromModel. */
structure: {
...FullMVSTreeSchema.nodes.structure,
parent: ['model'],
params: omitObjectKeys(FullMVSTreeSchema.nodes.structure.params, ['block_header', 'block_index', 'model_index'] as const),
params: SimpleParamsSchema(
omitObjectKeys(FullMVSTreeSchema.nodes.structure.params.fields, ['block_header', 'block_index', 'model_index'] as const)
),
},
}
});
@@ -60,5 +65,11 @@ export type MolstarKind = keyof typeof MolstarTreeSchema.nodes;
/** Node in a `MolstarTree` */
export type MolstarNode<TKind extends MolstarKind = MolstarKind> = NodeFor<typeof MolstarTreeSchema, TKind>
/** Params for a specific node kind in a `MolstarTree` */
export type MolstarNodeParams<TKind extends MolstarKind> = ParamsOfKind<MolstarTree, TKind>
/** Intermediate tree representation between `MVSTree` and a real Molstar state */
export type MolstarTree = TreeFor<typeof MolstarTreeSchema>
/** Any subtree in a `MolstarTree` (e.g. its root doesn't need to be 'root') */
export type MolstarSubtree<TKind extends MolstarKind = MolstarKind> = SubtreeOfKind<MolstarTree, TKind>

View File

@@ -1,12 +1,11 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { treeValidationIssues } from '../../generic/tree-schema';
import { MVSData } from '../../../mvs-data';
import { builderDemo } from '../mvs-builder';
import { MVSTreeSchema } from '../mvs-tree';
describe('mvs-builder', () => {
@@ -14,6 +13,6 @@ describe('mvs-builder', () => {
const mvsData = builderDemo();
expect(typeof mvsData.metadata.version).toEqual('string');
expect(typeof mvsData.metadata.timestamp).toEqual('string');
expect(treeValidationIssues(MVSTreeSchema, mvsData.root)).toEqual(undefined);
expect(MVSData.validationIssues(mvsData)).toEqual(undefined);
});
});

View File

@@ -1,14 +1,13 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { deepClone, pickObjectKeys } from '../../../../mol-util/object';
import { MVSData } from '../../mvs-data';
import { ParamsOfKind, SubTreeOfKind } from '../generic/tree-schema';
import { MVSDefaults } from './mvs-defaults';
import { MVSKind, MVSNode, MVSTree, MVSTreeSchema } from './mvs-tree';
import { GlobalMetadata, MVSData_State, Snapshot, SnapshotMetadata } from '../../mvs-data';
import { CustomProps } from '../generic/tree-schema';
import { MVSKind, MVSNode, MVSNodeParams, MVSSubtree } from './mvs-tree';
/** Create a new MolViewSpec builder containing only a root node. Example of MVS builder usage:
@@ -21,24 +20,26 @@ import { MVSKind, MVSNode, MVSTree, MVSTreeSchema } from './mvs-tree';
* console.log(JSON.stringify(builder.getState()));
* ```
*/
export function createMVSBuilder() {
return new Root();
export function createMVSBuilder(params: CustomAndRef = {}) {
return new Root(params);
}
/** Base class for MVS builder pointing to anything */
class _Base<TKind extends MVSKind> {
protected constructor(
constructor(
protected readonly _root: Root,
protected readonly _node: SubTreeOfKind<MVSTree, TKind>,
protected readonly _node: MVSSubtree<TKind>,
) { }
/** Create a new node, append as child to current _node, and return the new node */
protected addChild<TChildKind extends MVSKind>(kind: TChildKind, params: ParamsOfKind<MVSTree, TChildKind>) {
const allowedParamNames = Object.keys(MVSTreeSchema.nodes[kind].params) as (keyof ParamsOfKind<MVSTree, TChildKind>)[];
protected addChild<TChildKind extends MVSKind>(kind: TChildKind, params_: MVSNodeParams<TChildKind> & CustomAndRef) {
const { params, custom, ref } = splitParams<MVSNodeParams<TChildKind>>(params_);
const node = {
kind,
params: pickObjectKeys(params, allowedParamNames) as unknown,
} as SubTreeOfKind<MVSTree, TChildKind>;
params,
custom,
ref,
} as MVSSubtree<TChildKind>;
this._node.children ??= [];
this._node.children.push(node);
return node;
@@ -47,46 +48,53 @@ class _Base<TKind extends MVSKind> {
/** MVS builder pointing to the 'root' node */
export class Root extends _Base<'root'> {
constructor() {
const node: MVSNode<'root'> = { kind: 'root' };
export class Root extends _Base<'root'> implements FocusMixin, PrimitivesMixin {
constructor(params_: CustomAndRef) {
const { custom, ref } = params_;
const node: MVSNode<'root'> = { kind: 'root', custom, ref };
super(undefined as any, node);
(this._root as Root) = this;
}
/** Return the current state of the builder as object in MVS format. */
getState(metadata?: Partial<Pick<MVSData['metadata'], 'title' | 'description' | 'description_format'>>): MVSData {
getState(metadata?: Pick<GlobalMetadata, 'title' | 'description' | 'description_format'>): MVSData_State {
return {
root: deepClone(this._node),
metadata: {
...metadata,
version: `${MVSData.SupportedVersion}`,
timestamp: utcNowISO(),
},
metadata: GlobalMetadata.create(metadata),
};
}
// omitting `saveState`, filesystem operations are responsibility of the caller code (platform-dependent)
/** Return the current state of the builder as a snapshot object to be used in multi-state . */
getSnapshot(metadata: SnapshotMetadata): Snapshot {
return {
root: deepClone(this._node),
metadata: { ...metadata },
};
}
/** Add a 'camera' node and return builder pointing to the root. 'camera' node instructs to set the camera position and orientation. */
camera(params: ParamsOfKind<MVSTree, 'camera'>): Root {
camera(params: MVSNodeParams<'camera'> & CustomAndRef): Root {
this.addChild('camera', params);
return this;
}
/** Add a 'canvas' node and return builder pointing to the root. 'canvas' node sets canvas properties. */
canvas(params: ParamsOfKind<MVSTree, 'canvas'>): Root {
canvas(params: MVSNodeParams<'canvas'> & CustomAndRef): Root {
this.addChild('canvas', params);
return this;
}
/** Add a 'download' node and return builder pointing to it. 'download' node instructs to retrieve a data resource. */
download(params: ParamsOfKind<MVSTree, 'download'>): Download {
download(params: MVSNodeParams<'download'> & CustomAndRef): Download {
return new Download(this._root, this.addChild('download', params));
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
}
/** MVS builder pointing to a 'download' node */
export class Download extends _Base<'download'> {
/** Add a 'parse' node and return builder pointing to it. 'parse' node instructs to parse a data resource. */
parse(params: ParamsOfKind<MVSTree, 'parse'>) {
parse(params: MVSNodeParams<'parse'> & CustomAndRef) {
return new Parse(this._root, this.addChild('parse', params));
}
}
@@ -98,144 +106,236 @@ const StructureParamsSubsets = {
assembly: ['block_header', 'block_index', 'model_index', 'assembly_id'],
symmetry: ['block_header', 'block_index', 'model_index', 'ijk_min', 'ijk_max'],
symmetry_mates: ['block_header', 'block_index', 'model_index', 'radius'],
} satisfies { [kind in ParamsOfKind<MVSTree, 'structure'>['type']]: (keyof ParamsOfKind<MVSTree, 'structure'>)[] };
} satisfies { [kind in MVSNodeParams<'structure'>['type']]: (keyof MVSNodeParams<'structure'>)[] };
/** MVS builder pointing to a 'parse' node */
export class Parse extends _Base<'parse'> {
/** Add a 'structure' node representing a "model structure", i.e. includes all coordinates from the original model without applying any transformations.
* Return builder pointing to the new node. */
modelStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['model'][number]> = {}): Structure {
modelStructure(params: Pick<MVSNodeParams<'structure'>, typeof StructureParamsSubsets['model'][number]> & CustomAndRef = {}): Structure {
return new Structure(this._root, this.addChild('structure', {
type: 'model',
...pickObjectKeys(params, StructureParamsSubsets.model),
...pickObjectKeys(params, [...StructureParamsSubsets.model]),
custom: params.custom,
ref: params.ref,
}));
}
/** Add a 'structure' node representing an "assembly structure", i.e. may apply filters and symmetry operators to the original model coordinates.
* Return builder pointing to the new node. */
assemblyStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['assembly'][number]> = {}): Structure {
assemblyStructure(params: Pick<MVSNodeParams<'structure'>, typeof StructureParamsSubsets['assembly'][number]> & CustomAndRef = {}): Structure {
return new Structure(this._root, this.addChild('structure', {
type: 'assembly',
...pickObjectKeys(params, StructureParamsSubsets.assembly),
custom: params.custom,
ref: params.ref,
}));
}
/** Add a 'structure' node representing a "symmetry structure", i.e. applies symmetry operators to build crystal unit cells within given Miller indices.
* Return builder pointing to the new node. */
symmetryStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['symmetry'][number]> = {}): Structure {
symmetryStructure(params: Pick<MVSNodeParams<'structure'>, typeof StructureParamsSubsets['symmetry'][number]> & CustomAndRef = {}): Structure {
return new Structure(this._root, this.addChild('structure', {
type: 'symmetry',
...pickObjectKeys(params, StructureParamsSubsets.symmetry),
custom: params.custom,
ref: params.ref,
}));
}
/** Add a 'structure' node representing a "symmetry mates structure", i.e. applies symmetry operators to build asymmetric units within a radius from the original model.
* Return builder pointing to the new node. */
symmetryMatesStructure(params: Pick<ParamsOfKind<MVSTree, 'structure'>, typeof StructureParamsSubsets['symmetry_mates'][number]> = {}): Structure {
symmetryMatesStructure(params: Pick<MVSNodeParams<'structure'>, typeof StructureParamsSubsets['symmetry_mates'][number]> & CustomAndRef = {}): Structure {
return new Structure(this._root, this.addChild('structure', {
type: 'symmetry_mates',
...pickObjectKeys(params, StructureParamsSubsets.symmetry_mates),
custom: params.custom,
ref: params.ref,
}));
}
}
/** MVS builder pointing to a 'structure' node */
export class Structure extends _Base<'structure'> {
export class Structure extends _Base<'structure'> implements PrimitivesMixin {
/** Add a 'component' node and return builder pointing to it. 'component' node instructs to create a component (i.e. a subset of the parent structure). */
component(params: Partial<ParamsOfKind<MVSTree, 'component'>> = {}): Component {
const fullParams = { ...params, selector: params.selector ?? MVSDefaults.component.selector };
component(params: Partial<MVSNodeParams<'component'>> & CustomAndRef = {}): Component {
const fullParams = { ...params, selector: params.selector ?? 'all' };
return new Component(this._root, this.addChild('component', fullParams));
}
/** Add a 'component_from_uri' node and return builder pointing to it. 'component_from_uri' node instructs to create a component defined by an external annotation resource. */
componentFromUri(params: ParamsOfKind<MVSTree, 'component_from_uri'>): Component {
componentFromUri(params: MVSNodeParams<'component_from_uri'> & CustomAndRef): Component {
return new Component(this._root, this.addChild('component_from_uri', params));
}
/** Add a 'component_from_source' node and return builder pointing to it. 'component_from_source' node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
componentFromSource(params: ParamsOfKind<MVSTree, 'component_from_source'>): Component {
componentFromSource(params: MVSNodeParams<'component_from_source'> & CustomAndRef): Component {
return new Component(this._root, this.addChild('component_from_source', params));
}
/** Add a 'label_from_uri' node and return builder pointing back to the structure node. 'label_from_uri' node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
labelFromUri(params: ParamsOfKind<MVSTree, 'label_from_uri'>): Structure {
labelFromUri(params: MVSNodeParams<'label_from_uri'> & CustomAndRef): Structure {
this.addChild('label_from_uri', params);
return this;
}
/** Add a 'label_from_source' node and return builder pointing back to the structure node. 'label_from_source' node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
labelFromSource(params: ParamsOfKind<MVSTree, 'label_from_source'>): Structure {
labelFromSource(params: MVSNodeParams<'label_from_source'> & CustomAndRef): Structure {
this.addChild('label_from_source', params);
return this;
}
/** Add a 'tooltip_from_uri' node and return builder pointing back to the structure node. 'tooltip_from_uri' node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
tooltipFromUri(params: ParamsOfKind<MVSTree, 'tooltip_from_uri'>): Structure {
tooltipFromUri(params: MVSNodeParams<'tooltip_from_uri'> & CustomAndRef): Structure {
this.addChild('tooltip_from_uri', params);
return this;
}
/** Add a 'tooltip_from_source' node and return builder pointing back to the structure node. 'tooltip_from_source' node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
tooltipFromSource(params: ParamsOfKind<MVSTree, 'tooltip_from_source'>): Structure {
tooltipFromSource(params: MVSNodeParams<'tooltip_from_source' & CustomAndRef>): Structure {
this.addChild('tooltip_from_source', params);
return this;
}
/** Add a 'transform' node and return builder pointing back to the structure node. 'transform' node instructs to rotate and/or translate structure coordinates. */
transform(params: ParamsOfKind<MVSTree, 'transform'> = {}): Structure {
transform(params: MVSNodeParams<'transform'> & CustomAndRef = {}): Structure {
if (params.rotation && params.rotation.length !== 9) {
throw new Error('ValueError: `rotation` parameter must be an array of 9 numbers');
}
this.addChild('transform', params);
return this;
}
primitives = bindMethod(this, PrimitivesMixinImpl, 'primitives');
primitives_from_uri = bindMethod(this, PrimitivesMixinImpl, 'primitives_from_uri');
}
/** MVS builder pointing to a 'component' or 'component_from_uri' or 'component_from_source' node */
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> {
export class Component extends _Base<'component' | 'component_from_uri' | 'component_from_source'> implements FocusMixin {
/** Add a 'representation' node and return builder pointing to it. 'representation' node instructs to create a visual representation of a component. */
representation(params: Partial<ParamsOfKind<MVSTree, 'representation'>> = {}): Representation {
const fullParams: ParamsOfKind<MVSTree, 'representation'> = { ...params, type: params.type ?? 'cartoon' };
representation(params: Partial<MVSNodeParams<'representation'>> & CustomAndRef = {}): Representation {
const fullParams: MVSNodeParams<'representation'> = { ...params, type: params.type ?? 'cartoon' };
return new Representation(this._root, this.addChild('representation', fullParams));
}
/** Add a 'label' node and return builder pointing back to the component node. 'label' node instructs to add a label (textual visual representation) to a component. */
label(params: ParamsOfKind<MVSTree, 'label'>): Component {
label(params: MVSNodeParams<'label'> & CustomAndRef): Component {
this.addChild('label', params);
return this;
}
/** Add a 'tooltip' node and return builder pointing back to the component node. 'tooltip' node instructs to add a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
tooltip(params: ParamsOfKind<MVSTree, 'tooltip'>): Component {
tooltip(params: MVSNodeParams<'tooltip'> & CustomAndRef): Component {
this.addChild('tooltip', params);
return this;
}
/** Add a 'focus' node and return builder pointing back to the component node. 'focus' node instructs to set the camera focus to a component (zoom in). */
focus(params: ParamsOfKind<MVSTree, 'focus'> = {}): Component {
this.addChild('focus', params);
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
/** MVS builder pointing to a 'representation' node */
export class Representation extends _Base<'representation'> {
/** Add a 'color' node and return builder pointing back to the representation node. 'color' node instructs to apply color to a visual representation. */
color(params: ParamsOfKind<MVSTree, 'color'>): Representation {
color(params: MVSNodeParams<'color'> & CustomAndRef): Representation {
this.addChild('color', params);
return this;
}
/** Add a 'color_from_uri' node and return builder pointing back to the representation node. 'color_from_uri' node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
colorFromUri(params: ParamsOfKind<MVSTree, 'color_from_uri'>): Representation {
colorFromUri(params: MVSNodeParams<'color_from_uri'> & CustomAndRef): Representation {
this.addChild('color_from_uri', params);
return this;
}
/** Add a 'color_from_source' node and return builder pointing back to the representation node. 'color_from_source' node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
colorFromSource(params: ParamsOfKind<MVSTree, 'color_from_source'>): Representation {
colorFromSource(params: MVSNodeParams<'color_from_source'> & CustomAndRef): Representation {
this.addChild('color_from_source', params);
return this;
}
/** Add an 'opacity' node and return builder pointing back to the representation node. 'opacity' node instructs to customize opacity/transparency of a visual representation. */
opacity(params: MVSNodeParams<'opacity'> & CustomAndRef): Representation {
this.addChild('opacity', params);
return this;
}
}
type MVSPrimitiveSubparams<TKind extends MVSNodeParams<'primitive'>['kind']> = Omit<Extract<MVSNodeParams<'primitive'>, { kind: TKind }>, 'kind'>;
/** MVS builder pointing to a 'primitives' node */
class Primitives extends _Base<'primitives'> implements FocusMixin {
/** Construct custom meshes/shapes in a low-level fashion by providing vertices and indices. */
mesh(params: MVSPrimitiveSubparams<'mesh'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'mesh', ...params });
return this;
}
/** Construct custom set of lines in a low-level fashion by providing vertices and indices. */
lines(params: MVSPrimitiveSubparams<'lines'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'lines', ...params });
return this;
}
/** Defines a tube (3D cylinder), connecting a start and an end point. */
tube(params: MVSPrimitiveSubparams<'tube'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'tube', ...params });
return this;
}
/** Defines a tube, connecting a start and an end point, with label containing distance between start and end. */
distance(params: MVSPrimitiveSubparams<'distance_measurement'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'distance_measurement', ...params });
return this;
}
/** Defines a label. */
label(params: MVSPrimitiveSubparams<'label'> & CustomAndRef): Primitives {
this.addChild('primitive', { kind: 'label', ...params });
return this;
}
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
/** MVS builder pointing to a 'primitives_from_uri' node */
class PrimitivesFromUri extends _Base<'primitives_from_uri'> implements FocusMixin {
focus = bindMethod(this, FocusMixinImpl, 'focus');
}
// MIXINS
type Constructor<T> = new (...args: any[]) => T;
/** Fake interface for typing tweaks */
interface Self { '@type': 'self' }
type ReplaceSelf<TFunction, TSelf> = TFunction extends (...args: infer TArgs) => Self ? (...args: TArgs) => TSelf : TFunction;
function bindMethod<O extends _Base<any>, C extends Constructor<_Base<any>>, M extends keyof InstanceType<C>>(thisObj: O, mixin: C, methodName: M): ReplaceSelf<InstanceType<C>[M], O> {
return mixin.prototype[methodName].bind(thisObj);
}
// This mixin implementation is really ugly but couldn't be bothered (running into TS2502: 'Root' is referenced directly or indirectly in its own type annotation)
interface FocusMixin {
/** Add a 'focus' node and return builder pointing back to the original node. 'focus' node instructs to set the camera focus to a component (zoom in). */
focus(params: MVSNodeParams<'focus'> & CustomAndRef): any,
}
class FocusMixinImpl extends _Base<MVSKind> implements FocusMixin {
focus(params: MVSNodeParams<'focus'> & CustomAndRef = {}): Self {
this.addChild('focus', params);
return this as unknown as Self;
}
};
interface PrimitivesMixin {
/** Allows the definition of a (group of) geometric primitives. You can add any number of primitives and then assign shared options (color, opacity etc.). */
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef): Primitives,
/** Allows the definition of a (group of) geometric primitives provided dynamically. */
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri,
};
class PrimitivesMixinImpl extends _Base<MVSKind> implements PrimitivesMixin {
primitives(params: MVSNodeParams<'primitives'> & CustomAndRef = {}): Primitives {
return new Primitives(this._root, this.addChild('primitives', params));
}
primitives_from_uri(params: MVSNodeParams<'primitives_from_uri'> & CustomAndRef): PrimitivesFromUri {
return new PrimitivesFromUri(this._root, this.addChild('primitives_from_uri', params));
}
};
/** Demonstration of usage of MVS builder */
export function builderDemo() {
const builder = createMVSBuilder();
builder.canvas({ background_color: 'white' });
const struct = builder.download({ url: 'https://www.ebi.ac.uk/pdbe/entry-files/download/1og2_updated.cif' }).parse({ format: 'mmcif' }).modelStructure();
struct.component().representation().color({ color: 'white' });
struct.component({ selector: 'ligand' }).representation({ type: 'ball_and_stick' })
struct.component({ selector: 'ligand' }).representation({ type: 'ball_and_stick', custom: { repr_quality: 'high' }, ref: 'Ligand' })
.color({ color: '#555555' })
.color({ selector: { type_symbol: 'N' }, color: '#3050F8' })
.color({ selector: { type_symbol: 'O' }, color: '#FF0D0D' })
@@ -255,7 +355,12 @@ export function builderDemo() {
return builder.getState();
}
/** Return the current universal time, in ISO format, e.g. '2023-11-24T10:45:49.873Z' */
function utcNowISO(): string {
return new Date().toISOString();
export interface CustomAndRef {
custom?: CustomProps,
ref?: string,
};
function splitParams<TParams extends {}>(params_custom_ref: TParams & CustomAndRef): { params: TParams, custom?: CustomProps, ref?: string } {
const { custom, ref, ...params } = params_custom_ref;
return { params: params as TParams, custom, ref };
}

View File

@@ -1,105 +0,0 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
*/
import { DefaultsForTree } from '../generic/tree-schema';
import { MVSTreeSchema } from './mvs-tree';
/** Default values for params in `MVSTree` */
export const MVSDefaults = {
root: {},
download: {
},
parse: {
},
structure: {
block_header: null,
block_index: 0,
model_index: 0,
assembly_id: null,
radius: 5,
ijk_min: [-1, -1, -1],
ijk_max: [1, 1, 1],
},
component: {
selector: 'all' as const,
},
component_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'component',
field_values: null,
},
component_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'component',
field_values: null,
},
representation: {
},
color: {
selector: 'all' as const,
},
color_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'color',
},
color_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'color',
},
label: {
},
label_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'label',
},
label_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'label',
},
tooltip: {
},
tooltip_from_uri: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'tooltip',
},
tooltip_from_source: {
block_header: null,
block_index: 0,
category_name: null,
field_name: 'tooltip',
},
focus: {
direction: [0, 0, -1],
up: [0, 1, 0],
},
transform: {
rotation: [1, 0, 0, 0, 1, 0, 0, 0, 1], // 3x3 identitity matrix
translation: [0, 0, 0],
},
canvas: {
},
camera: {
up: [0, 1, 0],
},
} satisfies DefaultsForTree<typeof MVSTreeSchema>;
/** Color to be used e.g. for representations without 'color' node */
export const DefaultColor = 'white';

View File

@@ -0,0 +1,115 @@
/**
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { bool, float, int, mapping, nullable, OptionalField, RequiredField, str } from '../generic/field-schema';
import { SimpleParamsSchema, UnionParamsSchema } from '../generic/params-schema';
import { ColorT, FloatList, IntList, PrimitivePositionT } from './param-types';
const _TubeBase = {
/** Start point of the tube. */
start: RequiredField(PrimitivePositionT, 'Start point of the tube.'),
/** End point of the tube. */
end: RequiredField(PrimitivePositionT, 'End point of the tube.'),
/** Tube radius (in Angstroms). */
radius: OptionalField(float, 0.05, 'Tube radius (in Angstroms).'),
/** Length of each dash and gap between dashes. If not specified (null), draw full line. */
dash_length: OptionalField(nullable(float), null, 'Length of each dash and gap between dashes. If not specified (null), draw full line.'),
/** Color of the tube. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the tube. If not specified, uses the parent primitives group `color`.'),
};
const MeshParams = {
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
/** 3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...). */
indices: RequiredField(IntList, '3*n_triangles length array of indices into vertices that form triangles (t1_1, t1_2, t1_3, ...).'),
/** Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i). */
triangle_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each triangle is considered a separate group (triangle i = group i).'),
/** Assign a color to each group. Where not assigned, uses `color`. */
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
/** Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the triangles and wireframe. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
/** Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the mesh. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
/** Determine whether to render triangles of the mesh. */
show_triangles: OptionalField(bool, true, 'Determine whether to render triangles of the mesh.'),
/** Determine whether to render wireframe of the mesh. */
show_wireframe: OptionalField(bool, false, 'Determine whether to render wireframe of the mesh.'),
/** Wireframe line width (in screen-space units). */
wireframe_width: OptionalField(float, 1, 'Wireframe line width (in screen-space units).'),
/** Wireframe color. If not specified, uses `group_colors`. */
wireframe_color: OptionalField(nullable(ColorT), null, 'Wireframe color. If not specified, uses `group_colors`.'),
};
const LinesParams = {
/** 3*n_vertices length array of floats with vertex position (x1, y1, z1, ...). */
vertices: RequiredField(FloatList, '3*n_vertices length array of floats with vertex position (x1, y1, z1, ...).'),
/** 2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...). */
indices: RequiredField(IntList, '2*n_lines length array of indices into vertices that form lines (l1_1, l1_2, ...).'),
/** Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i). */
line_groups: OptionalField(nullable(IntList), null, 'Assign a number to each triangle to group them. If not specified, each line is considered a separate group (line i = group i).'),
/** Assign a color to each group. Where not assigned, uses `color`. */
group_colors: OptionalField(mapping(int, ColorT), {}, 'Assign a color to each group. Where not assigned, uses `color`.'),
/** Assign a tooltip to each group. Where not assigned, uses `tooltip`. */
group_tooltips: OptionalField(mapping(int, str), {}, 'Assign a tooltip to each group. Where not assigned, uses `tooltip`.'),
/** Assign a line width to each group. Where not assigned, uses `width`. */
group_widths: OptionalField(mapping(int, float), {}, 'Assign a line width to each group. Where not assigned, uses `width`.'),
/** Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`. */
color: OptionalField(nullable(ColorT), null, 'Color of the lines. Can be overwritten by `group_colors`. If not specified, uses the parent primitives group `color`.'),
/** Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip shown when hovering over the lines. Can be overwritten by `group_tooltips`. If not specified, uses the parent primitives group `tooltip`.'),
/** Line width (in screen-space units). Can be overwritten by `group_widths`. */
width: OptionalField(float, 1, 'Line width (in screen-space units). Can be overwritten by `group_widths`.'),
};
const TubeParams = {
..._TubeBase,
/** Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`. */
tooltip: OptionalField(nullable(str), null, 'Tooltip to show when hovering over the tube. If not specified, uses the parent primitives group `tooltip`.'),
};
const DistanceMeasurementParams = {
..._TubeBase,
/** Template used to construct the label. Use {{distance}} as placeholder for the distance. */
label_template: OptionalField(str, '{{distance}}', 'Template used to construct the label. Use {{distance}} as placeholder for the distance.'),
/** Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min). */
label_size: OptionalField(nullable(float), null, 'Size of the label (text height in Angstroms). If not specified, size will be relative to the distance (see label_auto_size_scale, label_auto_size_min).'),
/** Scaling factor for relative size. */
label_auto_size_scale: OptionalField(float, 0.1, 'Scaling factor for relative size.'),
/** Minimum size for relative size. */
label_auto_size_min: OptionalField(float, 0, 'Minimum size for relative size.'),
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
};
const PrimitiveLabelParams = {
/** Position of this label. */
position: RequiredField(PrimitivePositionT, 'Position of this label.'),
/** The label. */
text: RequiredField(str, 'The label.'),
/** Size of the label (text height in Angstroms). */
label_size: OptionalField(float, 1, 'Size of the label (text height in Angstroms).'),
/** Color of the label. If not specified, uses the parent primitives group `label_color`. */
label_color: OptionalField(nullable(ColorT), null, 'Color of the label. If not specified, uses the parent primitives group `label_color`.'),
/** Camera-facing offset to prevent overlap with geometry. */
label_offset: OptionalField(float, 0, 'Camera-facing offset to prevent overlap with geometry.'),
};
export const MVSPrimitiveParams = UnionParamsSchema(
'kind',
'Kind of geometrical primitive',
{
'mesh': SimpleParamsSchema(MeshParams),
'lines': SimpleParamsSchema(LinesParams),
'tube': SimpleParamsSchema(TubeParams),
'distance_measurement': SimpleParamsSchema(DistanceMeasurementParams),
'label': SimpleParamsSchema(PrimitiveLabelParams),
},
);

View File

@@ -1,12 +1,15 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import { OptionalField, RequiredField, float, int, list, nullable, str, tuple, union } from '../generic/params-schema';
import { NodeFor, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StructureTypeT, Vector3 } from './param-types';
import { float, int, list, literal, nullable, OptionalField, RequiredField, str, tuple, union } from '../generic/field-schema';
import { SimpleParamsSchema } from '../generic/params-schema';
import { NodeFor, ParamsOfKind, SubtreeOfKind, TreeFor, TreeSchema, TreeSchemaWithAllRequired } from '../generic/tree-schema';
import { MVSPrimitiveParams } from './mvs-tree-primitives';
import { ColorT, ComponentExpressionT, ComponentSelectorT, Matrix, ParseFormatT, RepresentationTypeT, SchemaFormatT, SchemaT, StrList, StructureTypeT, Vector3 } from './param-types';
const _DataFromUriParams = {
@@ -17,29 +20,28 @@ const _DataFromUriParams = {
/** Annotation schema defines what fields in the annotation will be taken into account. */
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
/** Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `format` is `"cif"` or `"bcif"` and `block_header` is `null`).'),
/** Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used. */
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from (only applies when `format` is `"cif"` or `"bcif"`). If `null`, the first category in the block is used.'),
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
};
const _DataFromSourceParams = {
/** Annotation schema defines what fields in the annotation will be taken into account. */
schema: RequiredField(SchemaT, 'Annotation schema defines what fields in the annotation will be taken into account.'),
/** Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read annotation from. If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read annotation from (only applies when `block_header` is `null`).'),
/** Name of the CIF category to read annotation from. If `null`, the first category in the block is used. */
category_name: OptionalField(nullable(str), 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
category_name: OptionalField(nullable(str), null, 'Name of the CIF category to read annotation from. If `null`, the first category in the block is used.'),
/** Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...). The default value is 'color'/'label'/'tooltip'/'component' depending on the node type */
field_name: OptionalField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
field_name: RequiredField(str, 'Name of the column in CIF or field name (key) in JSON that contains the dependent variable (color/label/tooltip/component_id...).'),
};
/** Schema for `MVSTree` (MolViewSpec tree) */
export const MVSTreeSchema = TreeSchema({
rootKind: 'root',
@@ -48,225 +50,295 @@ export const MVSTreeSchema = TreeSchema({
root: {
description: 'Auxiliary node kind that only appears as the tree root.',
parent: [],
params: {
},
params: SimpleParamsSchema({
}),
},
/** This node instructs to retrieve a data resource. */
download: {
description: 'This node instructs to retrieve a data resource.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** URL of the data resource. */
url: RequiredField(str, 'URL of the data resource.'),
},
}),
},
/** This node instructs to parse a data resource. */
parse: {
description: 'This node instructs to parse a data resource.',
parent: ['download'],
params: {
params: SimpleParamsSchema({
/** Format of the input data resource. */
format: RequiredField(ParseFormatT, 'Format of the input data resource.'),
},
}),
},
/** This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation. */
structure: {
description: 'This node instructs to create a structure from a parsed data resource. "Structure" refers to an internal representation of molecular coordinates without any visual representation.',
parent: ['parse'],
params: {
params: SimpleParamsSchema({
/** Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model). */
type: RequiredField(StructureTypeT, 'Type of structure to be created (`"model"` for original model coordinates, `"assembly"` for assembly structure, `"symmetry"` for a set of crystal unit cells based on Miller indices, `"symmetry_mates"` for a set of asymmetric units within a radius from the original model).'),
/** Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`. */
block_header: OptionalField(nullable(str), 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
block_header: OptionalField(nullable(str), null, 'Header of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF). If `null`, block is selected based on `block_index`.'),
/** 0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`). */
block_index: OptionalField(int, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
block_index: OptionalField(int, 0, '0-based index of the CIF block to read coordinates from (only applies when the input data are from CIF or BinaryCIF and `block_header` is `null`).'),
/** 0-based index of model in case the input data contain multiple models. */
model_index: OptionalField(int, '0-based index of model in case the input data contain multiple models.'),
model_index: OptionalField(int, 0, '0-based index of model in case the input data contain multiple models.'),
/** Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected. */
assembly_id: OptionalField(nullable(str), 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
assembly_id: OptionalField(nullable(str), null, 'Assembly identifier (only applies when `kind` is `"assembly"`). If `null`, the first assembly is selected.'),
/** Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`). */
radius: OptionalField(float, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
radius: OptionalField(float, 5, 'Distance (in Angstroms) from the original model in which asymmetric units should be included (only applies when `kind` is `"symmetry_mates"`).'),
/** Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`). */
ijk_min: OptionalField(tuple([int, int, int]), 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
ijk_min: OptionalField(tuple([int, int, int]), [-1, -1, -1], 'Miller indices of the bottom-left unit cell to be included (only applies when `kind` is `"symmetry"`).'),
/** Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`). */
ijk_max: OptionalField(tuple([int, int, int]), 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
},
ijk_max: OptionalField(tuple([int, int, int]), [1, 1, 1], 'Miller indices of the top-right unit cell to be included (only applies when `kind` is `"symmetry"`).'),
}),
},
/** This node instructs to rotate and/or translate structure coordinates. */
transform: {
description: 'This node instructs to rotate and/or translate structure coordinates.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
/** Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation). */
rotation: OptionalField(Matrix, 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
rotation: OptionalField(Matrix, [1, 0, 0, 0, 1, 0, 0, 0, 1], 'Rotation matrix (3x3 matrix flattened in column major format (j*3+i indexing), this is equivalent to Fortran-order in numpy). This matrix will multiply the structure coordinates from the left. The default value is the identity matrix (corresponds to no rotation).'),
/** Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation). */
translation: OptionalField(Vector3, 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
},
translation: OptionalField(Vector3, [0, 0, 0], 'Translation vector, applied to the structure coordinates after rotation. The default value is the zero vector (corresponds to no translation).'),
}),
},
/** This node instructs to create a component (i.e. a subset of the parent structure). */
component: {
description: 'This node instructs to create a component (i.e. a subset of the parent structure).',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
/** Defines what part of the parent structure should be included in this component. */
selector: RequiredField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines what part of the parent structure should be included in this component.'),
},
}),
},
/** This node instructs to create a component defined by an external annotation resource. */
component_from_uri: {
description: 'This node instructs to create a component defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
},
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
}),
},
/** This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
component_from_source: {
description: 'This node instructs to create a component defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
/** Name of the column in CIF or field name (key) in JSON that contains the component identifier. */
field_name: OptionalField(str, 'component', 'Name of the column in CIF or field name (key) in JSON that contains the component identifier.'),
/** List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation. */
field_values: OptionalField(nullable(list(str)), 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
},
field_values: OptionalField(nullable(list(str)), null, 'List of component identifiers (i.e. values in the field given by `field_name`) which should be included in this component. If `null`, component identifiers are ignored (all annotation rows are included), and `field_name` field can be dropped from the annotation.'),
}),
},
/** This node instructs to create a visual representation of a component. */
representation: {
description: 'This node instructs to create a visual representation of a component.',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Method of visual representation of the component. */
type: RequiredField(RepresentationTypeT, 'Method of visual representation of the component.'),
},
}),
},
/** This node instructs to apply color to a visual representation. */
color: {
description: 'This node instructs to apply color to a visual representation.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
/** Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
color: RequiredField(ColorT, 'Color to apply to the representation. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
/** Defines to what part of the representation this color should be applied. */
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'Defines to what part of the representation this color should be applied.'),
},
selector: OptionalField(union([ComponentSelectorT, ComponentExpressionT, list(ComponentExpressionT)]), 'all', 'Defines to what part of the representation this color should be applied.'),
}),
},
/** This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource. */
color_from_uri: {
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an external annotation resource.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
}),
},
/** This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
color_from_source: {
description: 'This node instructs to apply colors to a visual representation. The colors are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['representation'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the color. */
field_name: OptionalField(str, 'color', 'Name of the column in CIF or field name (key) in JSON that contains the color.'),
}),
},
/** This node instructs to apply opacity/transparency to a visual representation. */
opacity: {
description: 'This node instructs to apply opacity/transparency to a visual representation.',
parent: ['representation'],
params: SimpleParamsSchema({
/** Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque. */
opacity: RequiredField(float, 'Opacity of a representation. 0.0: fully transparent, 1.0: fully opaque.'),
}),
},
/** This node instructs to add a label (textual visual representation) to a component. */
label: {
description: 'This node instructs to add a label (textual visual representation) to a component.',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Content of the shown label. */
text: RequiredField(str, 'Content of the shown label.'),
},
}),
},
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource. */
label_from_uri: {
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
}),
},
/** This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
label_from_source: {
description: 'This node instructs to add labels (textual visual representations) to parts of a structure. The labels are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the label text. */
field_name: OptionalField(str, 'label', 'Name of the column in CIF or field name (key) in JSON that contains the label text.'),
}),
},
/** This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component). */
tooltip: {
description: 'This node instructs to add a tooltip to a component. "Tooltip" is a text which is not a part of the visualization but should be presented to the users when they interact with the component (typically, the tooltip will be shown somewhere on the screen when the user hovers over a visual representation of the component).',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
params: SimpleParamsSchema({
/** Content of the shown tooltip. */
text: RequiredField(str, 'Content of the shown tooltip.'),
},
}),
},
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource. */
tooltip_from_uri: {
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an external annotation resource.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromUriParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
}),
},
/** This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file. */
tooltip_from_source: {
description: 'This node instructs to add tooltips to parts of a structure. The tooltips are defined by an annotation resource included in the same file this structure was loaded from. Only applicable if the structure was loaded from an mmCIF or BinaryCIF file.',
parent: ['structure'],
params: {
params: SimpleParamsSchema({
..._DataFromSourceParams,
},
/** Name of the column in CIF or field name (key) in JSON that contains the tooltip text. */
field_name: OptionalField(str, 'tooltip', 'Name of the column in CIF or field name (key) in JSON that contains the tooltip text.'),
}),
},
/** This node instructs to set the camera focus to a component (zoom in). */
focus: {
description: 'This node instructs to set the camera focus to a component (zoom in).',
parent: ['component', 'component_from_uri', 'component_from_source'],
params: {
parent: ['root', 'component', 'component_from_uri', 'component_from_source', 'primitives', 'primitives_from_uri'],
params: SimpleParamsSchema({
/** Vector describing the direction of the view (camera position -> focused target). */
direction: OptionalField(Vector3, 'Vector describing the direction of the view (camera position -> focused target).'),
direction: OptionalField(Vector3, [0, 0, -1], 'Vector describing the direction of the view (camera position -> focused target).'),
/** Vector which will be aligned with the screen Y axis. */
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
},
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
/** Radius of the focused sphere (overrides `radius_factor` and `radius_extra`. */
radius: OptionalField(nullable(float), null, 'Radius of the focused sphere (overrides `radius_factor` and `radius_extra`).'),
/** Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent. */
radius_factor: OptionalField(float, 1, 'Radius of the focused sphere relative to the radius of parent component (default: 1). Focused radius = component_radius * radius_factor + radius_extent.'),
/** Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent. */
radius_extent: OptionalField(float, 0, 'Addition to the radius of the focused sphere, if computed from the radius of parent component (default: 0). Focused radius = component_radius * radius_factor + radius_extent.'),
}),
},
/** This node instructs to set the camera position and orientation. */
camera: {
description: 'This node instructs to set the camera position and orientation.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** Coordinates of the point in space at which the camera is pointing. */
target: RequiredField(Vector3, 'Coordinates of the point in space at which the camera is pointing.'),
/** Coordinates of the camera. */
position: RequiredField(Vector3, 'Coordinates of the camera.'),
/** Vector which will be aligned with the screen Y axis. */
up: OptionalField(Vector3, 'Vector which will be aligned with the screen Y axis.'),
},
up: OptionalField(Vector3, [0, 1, 0], 'Vector which will be aligned with the screen Y axis.'),
}),
},
/** This node sets canvas properties. */
canvas: {
description: 'This node sets canvas properties.',
parent: ['root'],
params: {
params: SimpleParamsSchema({
/** Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`). */
background_color: RequiredField(ColorT, 'Color of the canvas background. Can be either an X11 color name (e.g. `"red"`) or a hexadecimal code (e.g. `"#FF0011"`).'),
},
}),
},
primitives: {
description: 'This node groups a list of geometrical primitives',
parent: ['structure', 'root'],
params: SimpleParamsSchema({
/** Default color for primitives in this group. */
color: OptionalField(ColorT, 'white', 'Default color for primitives in this group.'),
/** Default label color for primitives in this group. */
label_color: OptionalField(ColorT, 'white', 'Default label color for primitives in this group.'),
/** Default tooltip for primitives in this group. */
tooltip: OptionalField(nullable(str), null, 'Default tooltip for primitives in this group.'),
/** Opacity of primitive geometry in this group. */
opacity: OptionalField(float, 1, 'Opacity of primitive geometry in this group.'),
/** Opacity of primitive labels in this group. */
label_opacity: OptionalField(float, 1, 'Opacity of primitive labels in this group.'),
/** Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices. */
instances: OptionalField(nullable(list(Matrix)), null, 'Instances of this primitive group defined as 4x4 column major (j * 4 + i indexing) transformation matrices.'),
}),
},
primitives_from_uri: {
description: 'This node loads a list of primitives from URI',
parent: ['structure', 'root'],
params: SimpleParamsSchema({
/** Location of the resource. */
uri: RequiredField(str, 'Location of the resource.'),
/** Format of the data. */
format: RequiredField(literal('mvs-node-json'), 'Format of the data.'),
/** List of nodes the data are referencing. */
references: OptionalField(StrList, [], 'List of nodes the data are referencing.'),
}),
},
primitive: {
description: 'This node represents a geometrical primitive',
parent: ['primitives'],
params: MVSPrimitiveParams,
},
}
});
/** Node kind in a `MVSTree` */
export type MVSKind = keyof typeof MVSTreeSchema.nodes
/** Node in a `MVSTree` */
export type MVSNode<TKind extends MVSKind = MVSKind> = NodeFor<typeof MVSTreeSchema, TKind>
/** Params for a specific node kind in a `MVSTree` */
export type MVSNodeParams<TKind extends MVSKind> = ParamsOfKind<MVSTree, TKind>
/** MolViewSpec tree */
export type MVSTree = TreeFor<typeof MVSTreeSchema>
/** Any subtree in a `MVSTree` (e.g. its root doesn't need to be 'root') */
export type MVSSubtree<TKind extends MVSKind = MVSKind> = SubtreeOfKind<MVSTree, TKind>
/** Schema for `MVSTree` (MolViewSpec tree with all params provided) */
export const FullMVSTreeSchema = TreeSchemaWithAllRequired(MVSTreeSchema);
/** MolViewSpec tree with all params provided */
export type FullMVSTree = TreeFor<typeof FullMVSTreeSchema>
export type FullMVSTree = TreeFor<typeof FullMVSTreeSchema>

View File

@@ -1,12 +1,13 @@
/**
* Copyright (c) 2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2023-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Adam Midlik <midlik@gmail.com>
* @author David Sehnal <david.sehnal@gmail.com>
*/
import * as iots from 'io-ts';
import { HexColor } from '../../helpers/utils';
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/params-schema';
import { HexColor, ColorName } from '../../helpers/utils';
import { ValueFor, float, int, list, literal, str, tuple, union } from '../generic/field-schema';
import { ColorNames } from '../../../../mol-util/color/names';
@@ -42,6 +43,7 @@ export const ComponentExpressionT = iots.partial({
atom_id: int,
atom_index: int,
});
export type ComponentExpressionT = ValueFor<typeof ComponentExpressionT>
/** `type` parameter values for `representation` node in MVS tree */
export const RepresentationTypeT = literal('ball_and_stick', 'cartoon', 'surface');
@@ -54,10 +56,22 @@ export const SchemaFormatT = literal('cif', 'bcif', 'json');
/** Parameter values for vector params, e.g. `position` */
export const Vector3 = tuple([float, float, float]);
export type Vector3 = ValueFor<typeof Vector3>
/** Parameter values for matrix params, e.g. `rotation` */
export const Matrix = list(float);
/** Primitives-related types */
export const PrimitiveComponentExpressionT = iots.partial({ structure_ref: str, expression_schema: SchemaT, expressions: list(ComponentExpressionT) });
export type PrimitiveComponentExpressionT = ValueFor<typeof PrimitiveComponentExpressionT>
export const PrimitivePositionT = iots.union([Vector3, ComponentExpressionT, list(PrimitiveComponentExpressionT)]);
export type PrimitivePositionT = ValueFor<typeof PrimitivePositionT>
export const FloatList = list(float);
export const IntList = list(int);
export const StrList = list(str);
/** `color` parameter values for `color` node in MVS tree */
export const HexColorT = new iots.Type<HexColor>(
'HexColor',
@@ -66,8 +80,29 @@ export const HexColorT = new iots.Type<HexColor>(
value => value
);
/** `color` parameter values for `color` node in MVS tree */
export const ColorNameT = new iots.Type<ColorName>(
'ColorName',
((value: any) => typeof value === 'string') as any,
(value, ctx) => ColorName.is(value) ? { _tag: 'Right', right: value } : { _tag: 'Left', left: [{ value: value, context: ctx, message: `"${value}" is not a valid hex color string` }] },
value => value
);
/** `color` parameter values for `color` node in MVS tree */
export const ColorNamesT = literal(...Object.keys(ColorNames) as (keyof ColorNames)[]);
/** `color` parameter values for `color` node in MVS tree */
export const ColorT = union([HexColorT, ColorNamesT]);
export const ColorT = union([ColorNameT, HexColorT]);
/** Type helpers */
export function isVector3(x: any): x is Vector3 {
return !!x && Array.isArray(x) && x.length === 3 && typeof x[0] === 'number';
}
export function isPrimitiveComponentExpressions(x: any): x is PrimitiveComponentExpressionT {
return !!x && Array.isArray(x.expressions);
}
export function isComponentExpression(x: any): x is ComponentExpressionT {
return !!x && typeof x === 'object' && !x.expressions;
}

View File

@@ -10,7 +10,7 @@ import { CustomProperty } from '../../../mol-model-props/common/custom-property'
const Colors = {
Bond: Color(0xffffff),
Error: Color(0x00ff00),
MissingCharge: Color(0xffffff),
MissingCharge: Color(0x66ff00),
Negative: Color(0xff0000),
Zero: Color(0xffffff),

View File

@@ -5,6 +5,7 @@ import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
import { CustomPropertyDescriptor } from '../../../mol-model/custom-property';
import { CustomModelProperty } from '../../../mol-model-props/common/custom-model-property';
import { arrayMinMax } from '../../../mol-util/array';
import { Column } from '../../../mol-data/db';
type TypeId = number;
type IdToCharge = Map<number, number>;
@@ -106,6 +107,8 @@ function getTypeIdToAtomIdToCharge(model: Model): SBNcbrPartialChargeData['typeI
for (let i = 0; i < rowCount; ++i) {
const typeId = typeIds.int(i);
const atomId = atomIds.int(i);
const isPresent = charges.valueKind(i) === Column.ValueKind.Present;
if (!isPresent) continue;
const charge = charges.float(i);
if (!atomIdToCharge.has(typeId)) atomIdToCharge.set(typeId, new Map());
atomIdToCharge.get(typeId)?.set(atomId, charge);

View File

@@ -53,7 +53,12 @@ export class VolumeApiV2 {
public async getEntryList(maxEntries: number, keyword?: string): Promise<{ [source: string]: string[] }> {
const response = await fetch(this.entryListUrl(maxEntries, keyword));
return await response.json();
if (response.ok) {
return await response.json();
} else {
console.error('Failed to fetch "Volume & Segmentation" entry list');
return {};
}
}
public async getMetadata(source: string, entryId: string): Promise<Metadata> {

View File

@@ -1,9 +1,10 @@
/**
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
* @author Gianluca Tomasello <giagitom@gmail.com>
* @author Herman Bergwerf <post@hbergwerf.nl>
*/
import { BehaviorSubject, Subject, Subscription, debounceTime, merge } from 'rxjs';
@@ -140,7 +141,7 @@ namespace Canvas3DContext {
preserveDrawingBuffer: true,
preferWebGl1: false,
handleResize: () => {},
handleResize: () => { },
};
export type Attribs = typeof DefaultAttribs
@@ -391,6 +392,8 @@ namespace Canvas3D {
let y = 0;
let width = 128;
let height = 128;
let canvasScaleRatioX = 1;
let canvasScaleRatioY = 1;
let forceNextRender = false;
let currentTime = 0;
@@ -644,7 +647,7 @@ namespace Canvas3D {
function identify(x: number, y: number): PickData | undefined {
const cam = p.camera.stereo.name === 'on' ? stereoCamera : camera;
return webgl.isContextLost ? undefined : pickHelper.identify(x, y, cam);
return webgl.isContextLost ? undefined : pickHelper.identify(x / canvasScaleRatioX, y / canvasScaleRatioY, cam);
}
function commit(isSynchronous: boolean = false) {
@@ -912,11 +915,12 @@ namespace Canvas3D {
debounceTime(p.userInteractionReleaseMs)
).subscribe(() => {
isActivelyInteracting = isDragging;
if (!isDragging) requestDraw();
if (!isDragging && passes.illumination.supported && p.illumination.enabled) {
requestDraw();
}
}),
];
//
if (isDebugMode && canvas) {
@@ -972,7 +976,10 @@ namespace Canvas3D {
reprRenderObjects.clear();
scene.clear();
helper.debug.clear();
if (fenceSync !== null) webgl.deleteSync(fenceSync);
if (fenceSync !== null) {
webgl.deleteSync(fenceSync);
fenceSync = null;
}
requestDraw();
reprCount.next(reprRenderObjects.size);
},
@@ -1141,7 +1148,10 @@ namespace Canvas3D {
renderer.dispose();
interactionHelper.dispose();
hiZ.dispose();
if (fenceSync !== null) webgl.deleteSync(fenceSync);
if (fenceSync !== null) {
webgl.deleteSync(fenceSync);
fenceSync = null;
}
removeConsoleStatsProvider(consoleStats);
}
@@ -1150,6 +1160,12 @@ namespace Canvas3D {
function updateViewport() {
const oldX = x, oldY = y, oldWidth = width, oldHeight = height;
const canvasRect = canvas?.getBoundingClientRect();
canvasScaleRatioX = (canvasRect?.width ?? gl.drawingBufferWidth) / gl.drawingBufferWidth;
if (!canvasScaleRatioX) canvasScaleRatioX = 1;
canvasScaleRatioY = (canvasRect?.height ?? gl.drawingBufferHeight) / gl.drawingBufferHeight;
if (!canvasScaleRatioY) canvasScaleRatioY = 1;
if (p.viewport.name === 'canvas') {
x = 0;
y = 0;
@@ -1176,7 +1192,7 @@ namespace Canvas3D {
pickHelper.setViewport(x, y, width, height);
renderer.setViewport(x, y, width, height);
Viewport.set(camera.viewport, x, y, width, height);
Viewport.set(controls.viewport, x, y, width, height);
Viewport.set(controls.viewport, x, y, width * canvasScaleRatioX, height * canvasScaleRatioY);
hiZ.setViewport(x, y, width, height);
}
}

View File

@@ -267,7 +267,7 @@ export class DpoitPass {
resources.texture('image-float32', 'rgba', 'float', 'nearest')
];
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
this.depthTextures = [
resources.texture('image-float32', 'rgba', 'float', 'nearest'),

View File

@@ -47,12 +47,13 @@ export class DrawPass {
private readonly drawTarget: RenderTarget;
readonly colorTarget: RenderTarget;
readonly transparentColorTarget: RenderTarget;
readonly depthTextureTransparent: Texture;
readonly depthTextureOpaque: Texture;
readonly packedDepth: boolean;
private depthTargetTransparent: RenderTarget;
readonly depthTargetTransparent: RenderTarget;
private depthTargetOpaque: RenderTarget | null;
private copyFboTarget: CopyRenderable;
@@ -91,6 +92,8 @@ export class DrawPass {
const { extensions, resources, isWebGL2 } = webgl;
this.drawTarget = createNullRenderTarget(webgl.gl);
this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
this.transparentColorTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.packedDepth = !extensions.depthTexture;
this.depthTargetTransparent = webgl.createRenderTarget(width, height);
@@ -129,6 +132,7 @@ export class DrawPass {
if (width !== w || height !== h) {
this.colorTarget.setSize(width, height);
this.depthTargetTransparent.setSize(width, height);
this.transparentColorTarget.setSize(width, height);
if (this.depthTargetOpaque) {
this.depthTargetOpaque.setSize(width, height);
@@ -163,13 +167,12 @@ export class DrawPass {
// render opaque primitives
if (scene.hasOpaque) {
renderer.renderDpoitOpaque(scene.primitives, camera, null);
renderer.renderOpaque(scene.primitives, camera);
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
if (outlineEnabled || dofEnabled) {
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -177,16 +180,14 @@ export class DrawPass {
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
// render transparent primitives
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
if (scene.opacityAverage < 1) {
const target = PostprocessingPass.isEnabled(postprocessingProps)
? this.postprocessing.target : this.colorTarget;
const target = isPostprocessingEnabled ? this.transparentColorTarget : this.colorTarget;
if (isPostprocessingEnabled) {
target.bind();
renderer.clear(false, false, true);
}
const dpoitTextures = this.dpoit.bind();
renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
@@ -206,9 +207,13 @@ export class DrawPass {
this.dpoit.render();
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
// render transparent volumes
if (scene.volumes.renderables.length > 0) {
renderer.renderDpoitVolume(scene.volumes, camera, this.depthTextureOpaque);
renderer.renderVolume(scene.volumes, camera, this.depthTextureOpaque);
}
}
@@ -220,13 +225,10 @@ export class DrawPass {
// render opaque primitives
if (scene.hasOpaque) {
renderer.renderWboitOpaque(scene.primitives, camera, null);
renderer.renderOpaque(scene.primitives, camera);
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -234,28 +236,38 @@ export class DrawPass {
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
// render transparent primitives
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
if (scene.opacityAverage < 1) {
const target = isPostprocessingEnabled ? this.transparentColorTarget : this.colorTarget;
if (isPostprocessingEnabled) {
target.bind();
renderer.clear(false, false, true);
}
// render transparent primitives and volumes
if (scene.opacityAverage < 1 || scene.volumes.renderables.length > 0) {
this.wboit.bind();
if (scene.opacityAverage < 1) {
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTextureOpaque);
}
if (scene.volumes.renderables.length > 0) {
renderer.renderWboitTransparent(scene.volumes, camera, this.depthTextureOpaque);
}
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTextureOpaque);
// evaluate wboit
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.target.bind();
} else {
this.colorTarget.bind();
}
target.bind();
this.wboit.render();
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
}
// render volumes
if (scene.volumes.renderables.length > 0) {
this.wboit.bind();
renderer.renderWboitTransparent(scene.volumes, camera, this.depthTextureOpaque);
// evaluate wboit
const target = isPostprocessingEnabled ? this.postprocessing.target : this.colorTarget;
target.bind();
this.wboit.render();
}
}
private _renderBlended(renderer: Renderer, camera: ICamera, scene: Scene, toDrawingBuffer: boolean, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
@@ -271,7 +283,7 @@ export class DrawPass {
renderer.clear(true);
if (scene.hasOpaque) {
renderer.renderBlendedOpaque(scene.primitives, camera, null);
renderer.renderOpaque(scene.primitives, camera);
}
if (!toDrawingBuffer) {
@@ -280,14 +292,11 @@ export class DrawPass {
if (this.depthTargetOpaque) {
this.depthTargetOpaque.bind();
renderer.clearDepth(true);
renderer.renderDepthOpaque(scene.primitives, camera, null);
renderer.renderDepthOpaque(scene.primitives, camera);
this.colorTarget.bind();
}
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
const dofEnabled = DofPass.isEnabled(postprocessingProps);
if (outlineEnabled || dofEnabled) {
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (scene.opacityAverage < 1) {
@@ -295,14 +304,39 @@ export class DrawPass {
}
}
if (PostprocessingPass.isEnabled(postprocessingProps)) {
// render transparent primitives
const isPostprocessingEnabled = PostprocessingPass.isEnabled(postprocessingProps);
if (scene.opacityAverage < 1) {
if (isPostprocessingEnabled) {
this.transparentColorTarget.bind();
renderer.clear(false, false, true);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(this.transparentColorTarget.framebuffer, 'depth');
} else {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.transparentColorTarget.framebuffer);
}
}
renderer.renderBlendedTransparent(scene.primitives, camera);
if (isPostprocessingEnabled) {
if (!this.packedDepth) {
this.depthTextureOpaque.detachFramebuffer(this.transparentColorTarget.framebuffer, 'depth');
} else {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.transparentColorTarget.framebuffer);
}
}
}
if (isPostprocessingEnabled) {
if (!this.packedDepth) {
this.depthTextureOpaque.detachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
} else {
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
}
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
@@ -322,7 +356,7 @@ export class DrawPass {
}
target.bind();
renderer.renderBlendedVolume(scene.volumes, camera, this.depthTextureOpaque);
renderer.renderVolume(scene.volumes, camera, this.depthTextureOpaque);
if (!this.packedDepth) {
this.depthTextureOpaque.attachFramebuffer(target.framebuffer, 'depth');
@@ -331,10 +365,8 @@ export class DrawPass {
}
target.bind();
}
}
if (scene.opacityAverage < 1) {
renderer.renderBlendedTransparent(scene.primitives, camera, null);
} else if (scene.opacityAverage < 1) {
renderer.renderBlendedTransparent(scene.primitives, camera);
}
}
@@ -376,7 +408,7 @@ export class DrawPass {
if (markingDepthTest && scene.markerAverage !== 1) {
this.marking.depthTarget.bind();
renderer.clear(false, true);
renderer.renderMarkingDepth(scene.primitives, camera, null);
renderer.renderMarkingDepth(scene.primitives, camera);
}
this.marking.maskTarget.bind();
@@ -443,7 +475,7 @@ export class DrawPass {
this.bloom.emissiveTarget.bind();
renderer.clear(false, true);
renderer.update(camera, scene);
renderer.renderEmissive(scene.primitives, camera, null);
renderer.renderEmissive(scene.primitives, camera);
}
if (!emissiveBloom || scene.emissiveAverage > 0) {

View File

@@ -32,6 +32,7 @@ import { TracingParams, TracingPass } from './tracing';
import { JitterVectors, MultiSampleProps } from './multi-sample';
import { compose_frag as multiSample_compose_frag } from '../../mol-gl/shader/compose.frag';
import { clamp, lerp } from '../../mol-math/interpolate';
import { SsaoProps } from './ssao';
type Props = {
transparentBackground: boolean;
@@ -64,7 +65,6 @@ export class IlluminationPass {
private readonly tracing: TracingPass;
private readonly transparentTarget: RenderTarget;
private readonly depthTargetTransparent: RenderTarget;
private readonly outputTarget: RenderTarget;
readonly packedDepth: boolean;
@@ -116,15 +116,14 @@ export class IlluminationPass {
const width = colorTarget.getWidth();
const height = colorTarget.getHeight();
this.tracing = new TracingPass(webgl, width, height);
this.tracing = new TracingPass(webgl, this.drawPass);
this.transparentTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
this.depthTargetTransparent = webgl.createRenderTarget(width, height);
this.outputTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.copyRenderable = createCopyRenderable(webgl, this.transparentTarget.texture);
this.composeRenderable = getComposeRenderable(webgl, this.tracing.accumulateTarget.texture, this.tracing.normalTextureOpaque, this.tracing.colorTextureOpaque, this.tracing.depthTextureOpaque, this.depthTargetTransparent.texture, this.drawPass.postprocessing.outline.target.texture, false);
this.composeRenderable = getComposeRenderable(webgl, this.tracing.accumulateTarget.texture, this.tracing.normalTextureOpaque, this.tracing.colorTextureOpaque, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, this.drawPass.postprocessing.outline.target.texture, this.transparentTarget.texture, this.drawPass.postprocessing.ssao.ssaoDepthTexture, this.drawPass.postprocessing.ssao.ssaoDepthTransparentTexture, false);
this.multiSampleComposeTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.multiSampleHoldTarget = webgl.createRenderTarget(width, height, false);
@@ -138,68 +137,77 @@ export class IlluminationPass {
if (isTimingMode) this.webgl.timer.mark('IlluminationPass.renderInput');
const { gl, state } = this.webgl;
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
const hasMarking = markingEnabled && scene.markerAverage > 0;
this.tracing.composeTarget.bind();
this.transparentTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
const outlineEnabled = PostprocessingPass.isTransparentOutlineEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
const dofEnabled = DofPass.isEnabled(props.postprocessing);
const ssaoEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
if (outlineEnabled || dofEnabled || ssaoEnabled) {
this.drawPass.depthTargetTransparent.bind();
renderer.clearDepth(true);
}
if (hasTransparent) {
if (this.drawPass.transparency === 'wboit') {
this.drawPass.wboit.bind();
renderer.renderWboitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque);
renderer.renderWboitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque);
if (scene.volumes.renderables.length > 0) {
renderer.renderWboitTransparent(scene.volumes, camera, this.tracing.depthTextureOpaque);
renderer.renderWboitTransparent(scene.volumes, camera, this.drawPass.depthTextureOpaque);
}
this.tracing.composeTarget.bind();
this.transparentTarget.bind();
this.drawPass.wboit.render();
} else if (this.drawPass.transparency === 'dpoit') {
const dpoitTextures = this.drawPass.dpoit.bind();
renderer.renderDpoitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque, dpoitTextures);
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
for (let i = 0, il = props.dpoitIterations; i < il; i++) {
if (isTimingMode) this.webgl.timer.mark('DpoitPass.layer');
const dpoitTextures = this.drawPass.dpoit.bindDualDepthPeeling();
renderer.renderDpoitTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque, dpoitTextures);
renderer.renderDpoitTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque, dpoitTextures);
this.tracing.composeTarget.bind();
this.transparentTarget.bind();
this.drawPass.dpoit.renderBlendBack();
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
}
// evaluate dpoit
this.tracing.composeTarget.bind();
this.transparentTarget.bind();
this.drawPass.dpoit.render();
if (scene.volumes.renderables.length > 0) {
renderer.renderDpoitVolume(scene.volumes, camera, this.tracing.depthTextureOpaque);
renderer.renderVolume(scene.volumes, camera, this.drawPass.depthTextureOpaque);
}
} else {
this.tracing.composeTarget.bind();
this.tracing.depthTextureOpaque.attachFramebuffer(this.tracing.composeTarget.framebuffer, 'depth');
renderer.renderBlendedTransparent(scene.primitives, camera, null);
this.tracing.depthTextureOpaque.detachFramebuffer(this.tracing.composeTarget.framebuffer, 'depth');
this.transparentTarget.bind();
this.drawPass.depthTextureOpaque.attachFramebuffer(this.transparentTarget.framebuffer, 'depth');
renderer.renderBlendedTransparent(scene.primitives, camera);
this.drawPass.depthTextureOpaque.detachFramebuffer(this.transparentTarget.framebuffer, 'depth');
if (scene.volumes.renderables.length > 0) {
renderer.renderBlendedVolume(scene.volumes, camera, this.tracing.depthTextureOpaque);
renderer.renderVolume(scene.volumes, camera, this.drawPass.depthTextureOpaque);
}
}
const outlineEnabled = PostprocessingPass.isEnabled(props.postprocessing) && PostprocessingPass.isTransparentOutlineEnabled(props.postprocessing) && !props.illumination.ignoreOutline;
const dofEnabled = DofPass.isEnabled(props.postprocessing);
if (outlineEnabled || dofEnabled) {
this.depthTargetTransparent.bind();
renderer.clearDepth(true);
if (outlineEnabled || dofEnabled || ssaoEnabled) {
this.drawPass.depthTargetTransparent.bind();
if (scene.opacityAverage < 1) {
renderer.renderDepthTransparent(scene.primitives, camera, this.tracing.depthTextureOpaque);
renderer.renderDepthTransparent(scene.primitives, camera, this.drawPass.depthTextureOpaque);
}
}
if (ssaoEnabled) {
this.drawPass.postprocessing.ssao.update(camera, scene, props.postprocessing.occlusion.params as SsaoProps, true);
this.drawPass.postprocessing.ssao.render(camera);
}
}
//
@@ -209,7 +217,7 @@ export class IlluminationPass {
if (markingDepthTest && scene.markerAverage !== 1) {
this.drawPass.marking.depthTarget.bind();
renderer.clear(false, true);
renderer.renderMarkingDepth(scene.primitives, camera, null);
renderer.renderMarkingDepth(scene.primitives, camera);
}
this.drawPass.marking.maskTarget.bind();
@@ -217,35 +225,14 @@ export class IlluminationPass {
renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.drawPass.marking.depthTarget.texture : null);
this.drawPass.marking.update(props.marking);
this.drawPass.marking.render(camera.viewport, this.tracing.composeTarget);
this.drawPass.marking.render(camera.viewport, this.transparentTarget);
}
//
if (antialiasingEnabled) {
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, this.transparentTarget, props.postprocessing);
} else {
if (this.copyRenderable.values.tColor.ref.value !== this.tracing.composeTarget.texture) {
ValueCell.update(this.copyRenderable.values.tColor, this.tracing.composeTarget.texture);
this.copyRenderable.update();
}
this.transparentTarget.bind();
state.enable(gl.SCISSOR_TEST);
state.disable(gl.BLEND);
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
state.colorMask(true, true, true, true);
state.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
this.copyRenderable.render();
}
this.tracing.composeTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.clear(gl.COLOR_BUFFER_BIT);
if (isTimingMode) this.webgl.timer.markEnd('IlluminationPass.renderInput');
}
@@ -263,7 +250,6 @@ export class IlluminationPass {
this.tracing.setSize(width, height);
this.transparentTarget.setSize(width, height);
this.depthTargetTransparent.setSize(width, height);
this.outputTarget.setSize(width, height);
ValueCell.update(this.copyRenderable.values.uTexSize, Vec2.set(this.copyRenderable.values.uTexSize.ref.value, width, height));
@@ -320,6 +306,11 @@ export class IlluminationPass {
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
const outlinesEnabled = props.postprocessing.outline.name === 'on' && !props.illumination.ignoreOutline;
const occlusionEnabled = PostprocessingPass.isTransparentSsaoEnabled(scene, props.postprocessing);
const markingEnabled = MarkingPass.isEnabled(props.marking);
const hasTransparent = scene.opacityAverage < 1;
const hasMarking = markingEnabled && scene.markerAverage > 0;
let needsUpdateCompose = false;
@@ -329,7 +320,7 @@ export class IlluminationPass {
}
if (props.postprocessing.outline.name === 'on') {
const { transparentOutline, outlineScale } = this.drawPass.postprocessing.outline.update(camera, props.postprocessing.outline.params, this.depthTargetTransparent.texture, this.tracing.depthTextureOpaque);
const { transparentOutline, outlineScale } = this.drawPass.postprocessing.outline.update(camera, props.postprocessing.outline.params, this.drawPass.depthTargetTransparent.texture, this.drawPass.depthTextureOpaque);
this.drawPass.postprocessing.outline.render();
ValueCell.update(this.composeRenderable.values.uOutlineColor, Color.toVec3Normalized(this.composeRenderable.values.uOutlineColor.ref.value, props.postprocessing.outline.params.color));
@@ -344,6 +335,21 @@ export class IlluminationPass {
}
}
if (this.composeRenderable.values.dOcclusionEnable.ref.value !== occlusionEnabled) {
needsUpdateCompose = true;
ValueCell.update(this.composeRenderable.values.dOcclusionEnable, occlusionEnabled);
}
if (props.postprocessing.occlusion.name === 'on') {
ValueCell.update(this.composeRenderable.values.uOcclusionColor, Color.toVec3Normalized(this.composeRenderable.values.uOcclusionColor.ref.value, props.postprocessing.occlusion.params.color));
}
const blendTransparency = hasTransparent || hasMarking;
if (this.composeRenderable.values.dBlendTransparency.ref.value !== blendTransparency) {
needsUpdateCompose = true;
ValueCell.update(this.composeRenderable.values.dBlendTransparency, blendTransparency);
}
ValueCell.updateIfChanged(this.composeRenderable.values.uNear, camera.near);
ValueCell.updateIfChanged(this.composeRenderable.values.uFar, camera.far);
ValueCell.updateIfChanged(this.composeRenderable.values.uFogFar, camera.fogFar);
@@ -384,16 +390,6 @@ export class IlluminationPass {
//
state.enable(gl.BLEND);
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
if (this.copyRenderable.values.tColor.ref.value !== this.transparentTarget.texture) {
ValueCell.update(this.copyRenderable.values.tColor, this.transparentTarget.texture);
this.copyRenderable.update();
}
this.copyRenderable.render();
//
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
renderer.setPixelRatio(this.webgl.pixelRatio);
renderer.setViewport(x, y, width, height);
@@ -431,13 +427,13 @@ export class IlluminationPass {
if (props.postprocessing.bloom.name === 'on') {
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
this.drawPass.bloom.update(this.tracing.shadedTextureOpaque, this.tracing.normalTextureOpaque, this.tracing.depthTextureOpaque, props.postprocessing.bloom.params);
this.drawPass.bloom.update(this.tracing.colorTextureOpaque, this.tracing.normalTextureOpaque, this.drawPass.depthTextureOpaque, props.postprocessing.bloom.params);
this.drawPass.bloom.render(camera.viewport, _toDrawingBuffer ? undefined : this._colorTarget);
}
if (props.postprocessing.dof.name === 'on') {
const _toDrawingBuffer = toDrawingBuffer || targetIsDrawingbuffer;
this.drawPass.dof.update(camera, this._colorTarget.texture, this.tracing.depthTextureOpaque, this.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
this.drawPass.dof.update(camera, this._colorTarget.texture, this.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
this.drawPass.dof.render(camera.viewport, _toDrawingBuffer ? undefined : swapTarget);
if (!_toDrawingBuffer) {
@@ -583,6 +579,10 @@ const ComposeSchema = {
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tTransparentColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dBlendTransparency: DefineSpec('boolean'),
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tSsaoDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
@@ -598,8 +598,10 @@ const ComposeSchema = {
uFogFar: UniformSpec('f'),
uFogColor: UniformSpec('v3'),
uOutlineColor: UniformSpec('v3'),
uOcclusionColor: UniformSpec('v3'),
uTransparentBackground: UniformSpec('b'),
dOcclusionEnable: DefineSpec('boolean'),
dOutlineEnable: DefineSpec('boolean'),
dOutlineScale: DefineSpec('number'),
dTransparentOutline: DefineSpec('boolean'),
@@ -607,12 +609,16 @@ const ComposeSchema = {
const ComposeShaderCode = ShaderCode('compose', quad_vert, compose_frag);
type ComposeRenderable = ComputeRenderable<Values<typeof ComposeSchema>>
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, transparentOutline: boolean): ComposeRenderable {
function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, outlinesTexture: Texture, transparentColorTexture: Texture, ssaoDepthOpaqueTexture: Texture, ssaoDepthTransparentTexture: Texture, transparentOutline: boolean): ComposeRenderable {
const values: Values<typeof ComposeSchema> = {
...QuadValues,
tColor: ValueCell.create(colorTexture),
tNormal: ValueCell.create(normalTexture),
tShaded: ValueCell.create(shadedTexture),
tTransparentColor: ValueCell.create(transparentColorTexture),
dBlendTransparency: ValueCell.create(true),
tSsaoDepth: ValueCell.create(ssaoDepthOpaqueTexture),
tSsaoDepthTransparent: ValueCell.create(ssaoDepthTransparentTexture),
tDepthOpaque: ValueCell.create(depthTextureOpaque),
tDepthTransparent: ValueCell.create(depthTextureTransparent),
tOutlines: ValueCell.create(outlinesTexture),
@@ -628,8 +634,10 @@ function getComposeRenderable(ctx: WebGLContext, colorTexture: Texture, normalTe
uFogFar: ValueCell.create(10000),
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
uOutlineColor: ValueCell.create(Vec3.create(0, 0, 0)),
uOcclusionColor: ValueCell.create(Vec3.create(0, 0, 0)),
uTransparentBackground: ValueCell.create(false),
dOcclusionEnable: ValueCell.create(false),
dOutlineEnable: ValueCell.create(false),
dOutlineScale: ValueCell.create(1),
dTransparentOutline: ValueCell.create(transparentOutline),

View File

@@ -182,16 +182,16 @@ export class PickPass {
private renderVariant(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, variant: 'pick' | 'depth', pickType: number) {
renderer.clear(false);
renderer.update(camera, scene);
renderer.renderPick(scene.primitives, camera, variant, null, pickType);
renderer.renderPick(scene.primitives, camera, variant, pickType);
if (helper.handle.isEnabled) {
renderer.renderPick(helper.handle.scene, camera, variant, null, pickType);
renderer.renderPick(helper.handle.scene, camera, variant, pickType);
}
if (helper.camera.isEnabled) {
helper.camera.update(camera);
renderer.update(helper.camera.camera, helper.camera.scene);
renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, null, pickType);
renderer.renderPick(helper.camera.scene, helper.camera.camera, variant, pickType);
}
}

View File

@@ -20,6 +20,7 @@ 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 { Scene } from '../../mol-gl/scene';
import { quad_vert } from '../../mol-gl/shader/quad.vert';
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
import { Color } from '../../mol-util/color';
@@ -30,7 +31,7 @@ import { BackgroundParams, BackgroundPass } from './background';
import { AssetManager } from '../../mol-util/assets';
import { Light } from '../../mol-gl/renderer';
import { CasParams, CasPass } from './cas';
import { DofParams } from './dof';
import { DofPass, DofParams } from './dof';
import { BloomParams } from './bloom';
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
@@ -40,8 +41,12 @@ import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
const PostprocessingSchema = {
...QuadSchema,
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tSsaoDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
tTransparentColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dBlendTransparency: DefineSpec('boolean'),
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'),
@@ -57,6 +62,9 @@ const PostprocessingSchema = {
uTransparentBackground: UniformSpec('b'),
dOcclusionEnable: DefineSpec('boolean'),
dOcclusionSingleDepth: DefineSpec('boolean'),
dOcclusionIncludeOpacity: DefineSpec('boolean'),
dOcclusionIncludeTransparency: DefineSpec('boolean'),
uOcclusionOffset: UniformSpec('v2'),
dShadowEnable: DefineSpec('boolean'),
@@ -67,12 +75,16 @@ const PostprocessingSchema = {
};
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, transparentColorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, ssaoDepthTransparentTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
const values: Values<typeof PostprocessingSchema> = {
...QuadValues,
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
tSsaoDepthTransparent: ValueCell.create(ssaoDepthTransparentTexture),
tColor: ValueCell.create(colorTexture),
tTransparentColor: ValueCell.create(transparentColorTexture),
dBlendTransparency: ValueCell.create(true),
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())),
@@ -88,6 +100,9 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
uTransparentBackground: ValueCell.create(false),
dOcclusionEnable: ValueCell.create(true),
dOcclusionSingleDepth: ValueCell.create(false),
dOcclusionIncludeOpacity: ValueCell.create(true),
dOcclusionIncludeTransparency: ValueCell.create(false),
uOcclusionOffset: ValueCell.create(Vec2.create(0, 0)),
dShadowEnable: ValueCell.create(false),
@@ -144,8 +159,20 @@ export class PostprocessingPass {
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
}
static isTransparentDepthRequired(scene: Scene, props: PostprocessingProps) {
return DofPass.isEnabled(props) || OutlinePass.isEnabled(props) && PostprocessingPass.isTransparentOutlineEnabled(props) || SsaoPass.isEnabled(props) && PostprocessingPass.isTransparentSsaoEnabled(scene, props);
}
static isTransparentOutlineEnabled(props: PostprocessingProps) {
return OutlinePass.isEnabled(props) && (props.outline.params as OutlineProps).includeTransparent;
return OutlinePass.isEnabled(props) && ((props.outline.params as OutlineProps).includeTransparent ?? true);
}
static isTransparentSsaoEnabled(scene: Scene, props: PostprocessingProps) {
return SsaoPass.isEnabled(props) && SsaoPass.isTransparentEnabled(scene, props.occlusion.params as SsaoProps);
}
static isSsaoEnabled(props: PostprocessingProps) {
return SsaoPass.isEnabled(props);
}
readonly target: RenderTarget;
@@ -158,18 +185,18 @@ export class PostprocessingPass {
readonly background: BackgroundPass;
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, readonly drawPass: DrawPass) {
const { colorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
const { colorTarget, transparentColorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
const width = colorTarget.getWidth();
const height = colorTarget.getHeight();
// needs to be linear for anti-aliasing pass
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque);
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque, depthTextureTransparent);
this.shadow = new ShadowPass(webgl, width, height, depthTextureOpaque);
this.outline = new OutlinePass(webgl, width, height, depthTextureTransparent, depthTextureOpaque);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, true);
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, transparentColorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, this.ssao.ssaoDepthTransparentTexture, true);
this.background = new BackgroundPass(webgl, assetManager, width, height);
}
@@ -188,7 +215,7 @@ export class PostprocessingPass {
this.background.setSize(width, height);
}
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
updateState(camera: ICamera, scene: Scene, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
let needsUpdateMain = false;
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
@@ -197,7 +224,14 @@ export class PostprocessingPass {
const occlusionEnabled = SsaoPass.isEnabled(props);
if (occlusionEnabled) {
this.ssao.update(camera, props.occlusion.params as SsaoProps);
const params = props.occlusion.params as SsaoProps;
this.ssao.update(camera, scene, params);
const includeTransparency = SsaoPass.isTransparentEnabled(scene, params);
if (this.renderable.values.dOcclusionIncludeTransparency.ref.value !== includeTransparency) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dOcclusionIncludeTransparency, includeTransparency);
}
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, params.color));
}
if (shadowsEnabled) {
@@ -245,6 +279,12 @@ export class PostprocessingPass {
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
}
const blendTransparency = scene.opacityAverage < 1;
if (this.renderable.values.dBlendTransparency.ref.value !== blendTransparency) {
needsUpdateMain = true;
ValueCell.update(this.renderable.values.dBlendTransparency, blendTransparency);
}
if (needsUpdateMain) {
this.renderable.update();
}
@@ -269,9 +309,9 @@ export class PostprocessingPass {
this.transparentBackground = value;
}
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
render(camera: ICamera, scene: Scene, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
this.updateState(camera, transparentBackground, backgroundColor, props, light, ambientColor);
this.updateState(camera, scene, transparentBackground, backgroundColor, props, light, ambientColor);
const { state } = this.webgl;
const { x, y, width, height } = camera.viewport;

View File

@@ -19,6 +19,7 @@ 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 { ICamera } from '../../mol-canvas3d/camera';
import { Scene } from '../../mol-gl/scene';
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';
@@ -51,6 +52,7 @@ export const SsaoParams = {
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)),
transparentThreshold: PD.Numeric(0.4, { min: 0, max: 1, step: 0.05 }),
};
export type SsaoProps = PD.Values<typeof SsaoParams>
@@ -81,30 +83,46 @@ export class SsaoPass {
return props.occlusion.name !== 'off';
}
static isTransparentEnabled(scene: Scene, props: SsaoProps) {
return scene.opacityAverage < 1 && scene.transparencyMin < props.transparentThreshold;
}
readonly target: RenderTarget;
private readonly framebuffer: Framebuffer;
private readonly blurFirstPassFramebuffer: Framebuffer;
private readonly blurSecondPassFramebuffer: Framebuffer;
private readonly downsampledDepthTarget: RenderTarget;
private readonly downsampleDepthRenderable: CopyRenderable;
private readonly downsampledDepthTargetOpaque: RenderTarget;
private readonly downsampleDepthRenderableOpaque: CopyRenderable;
private readonly depthHalfTarget: RenderTarget;
private readonly depthHalfRenderable: CopyRenderable;
private readonly depthHalfTargetOpaque: RenderTarget;
private readonly depthHalfRenderableOpaque: CopyRenderable;
private readonly depthQuarterTarget: RenderTarget;
private readonly depthQuarterRenderable: CopyRenderable;
private readonly depthQuarterTargetOpaque: RenderTarget;
private readonly depthQuarterRenderableOpaque: CopyRenderable;
private readonly downsampledDepthTargetTransparent: RenderTarget;
private readonly downsampleDepthRenderableTransparent: CopyRenderable;
private readonly depthHalfTargetTransparent: RenderTarget;
private readonly depthHalfRenderableTransparent: CopyRenderable;
private readonly depthQuarterTargetTransparent: RenderTarget;
private readonly depthQuarterRenderableTransparent: CopyRenderable;
readonly ssaoDepthTexture: Texture;
readonly ssaoDepthTransparentTexture: Texture;
private readonly depthBlurProxyTexture: Texture;
private depthTextureOpaque: Texture;
private depthTextureTransparent: Texture;
private readonly renderable: SsaoRenderable;
private readonly blurFirstPassRenderable: SsaoBlurRenderable;
private readonly blurSecondPassRenderable: SsaoBlurRenderable;
private depthTexture: Texture;
private nSamples: number;
private blurKernelSize: number;
private texSize: [number, number];
@@ -118,13 +136,18 @@ export class SsaoPass {
private levels: { radius: number, bias: number }[];
private getDepthTexture() {
return this.ssaoScale === 1 ? this.depthTexture : this.downsampledDepthTarget.texture;
return this.ssaoScale === 1 ? this.depthTextureOpaque : this.downsampledDepthTargetOpaque.texture;
}
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTexture: Texture) {
private getTransparentDepthTexture() {
return this.ssaoScale === 1 ? this.depthTextureTransparent : this.downsampledDepthTargetTransparent.texture;
}
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTextureOpaque: Texture, depthTextureTransparent: Texture) {
const { textureFloatLinear } = webgl.extensions;
this.depthTexture = depthTexture;
this.depthTextureOpaque = depthTextureOpaque;
this.depthTextureTransparent = depthTextureTransparent;
this.nSamples = 1;
this.blurKernelSize = 1;
@@ -147,33 +170,45 @@ export class SsaoPass {
const filter = textureFloatLinear ? 'linear' : 'nearest';
this.downsampledDepthTarget = packedDepth
this.downsampledDepthTargetOpaque = packedDepth
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture);
this.downsampleDepthRenderableOpaque = createCopyRenderable(webgl, depthTextureOpaque);
this.depthHalfTarget = packedDepth
const depthTexture = this.getDepthTexture();
this.depthHalfTargetOpaque = packedDepth
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(hw, hh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthHalfRenderable = createCopyRenderable(webgl, this.getDepthTexture());
this.depthHalfRenderableOpaque = createCopyRenderable(webgl, depthTexture);
this.depthQuarterTarget = packedDepth
this.depthQuarterTargetOpaque = packedDepth
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
this.depthQuarterRenderableOpaque = createCopyRenderable(webgl, this.depthHalfTargetOpaque.texture);
this.downsampledDepthTargetTransparent = webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba');
this.downsampleDepthRenderableTransparent = createCopyRenderable(webgl, depthTextureTransparent);
const transparentDepthTexture = this.getTransparentDepthTexture();
this.depthHalfTargetTransparent = webgl.createRenderTarget(hw, hh, false, 'uint8', 'linear', 'rgba');
this.depthHalfRenderableTransparent = createCopyRenderable(webgl, transparentDepthTexture);
this.depthQuarterTargetTransparent = webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba');
this.depthQuarterRenderableTransparent = createCopyRenderable(webgl, this.depthHalfTargetTransparent.texture);
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTexture.define(sw, sh);
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
this.ssaoDepthTransparentTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.ssaoDepthTransparentTexture.define(sw, sh);
this.depthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
this.depthBlurProxyTexture.define(sw, sh);
this.depthBlurProxyTexture.attachFramebuffer(this.blurFirstPassFramebuffer, 'color0');
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
this.renderable = getSsaoRenderable(webgl, this.getDepthTexture(), this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
this.renderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTargetOpaque.texture, this.depthQuarterTargetOpaque.texture, transparentDepthTexture, this.depthHalfTargetTransparent.texture, this.depthQuarterTargetTransparent.texture);
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTransparentTexture, 'horizontal');
this.blurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.depthBlurProxyTexture, 'vertical');
}
@@ -185,35 +220,51 @@ export class SsaoPass {
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.ssaoDepthTransparentTexture.define(sw, sh);
this.depthBlurProxyTexture.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));
this.downsampledDepthTargetOpaque.setSize(sw, sh);
this.depthHalfTargetOpaque.setSize(hw, hh);
this.depthQuarterTargetOpaque.setSize(qw, qh);
ValueCell.update(this.downsampleDepthRenderableOpaque.values.uTexSize, Vec2.set(this.downsampleDepthRenderableOpaque.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderableOpaque.values.uTexSize, Vec2.set(this.depthHalfRenderableOpaque.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderableOpaque.values.uTexSize, Vec2.set(this.depthQuarterRenderableOpaque.values.uTexSize.ref.value, qw, qh));
this.downsampledDepthTargetTransparent.setSize(sw, sh);
this.depthHalfTargetTransparent.setSize(hw, hh);
this.depthQuarterTargetTransparent.setSize(qw, qh);
ValueCell.update(this.downsampleDepthRenderableTransparent.values.uTexSize, Vec2.set(this.downsampleDepthRenderableTransparent.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderableTransparent.values.uTexSize, Vec2.set(this.depthHalfRenderableTransparent.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderableTransparent.values.uTexSize, Vec2.set(this.depthQuarterRenderableTransparent.values.uTexSize.ref.value, qw, qh));
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
const depthTexture = this.getDepthTexture();
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.renderable.values.tDepth, depthTexture);
const transparentDepthTexture = this.getTransparentDepthTexture();
this.depthHalfRenderable.update();
ValueCell.update(this.depthHalfRenderableOpaque.values.tColor, depthTexture);
ValueCell.update(this.depthHalfRenderableTransparent.values.tColor, transparentDepthTexture);
ValueCell.update(this.renderable.values.tDepth, depthTexture);
ValueCell.update(this.renderable.values.tDepthTransparent, transparentDepthTexture);
this.depthHalfRenderableOpaque.update();
this.depthHalfRenderableTransparent.update();
this.renderable.update();
}
}
update(camera: ICamera, props: SsaoProps) {
update(camera: ICamera, scene: Scene, props: SsaoProps, illuminationMode = false) {
let needsUpdateSsao = false;
let needsUpdateSsaoBlur = false;
let needsUpdateDepthHalf = false;
@@ -259,6 +310,19 @@ export class SsaoPass {
ValueCell.update(this.blurSecondPassRenderable.values.dOrthographic, orthographic);
}
const includeTransparent = SsaoPass.isTransparentEnabled(scene, props);
if (this.renderable.values.dIncludeTransparent.ref.value !== includeTransparent) {
needsUpdateSsao = true;
ValueCell.update(this.renderable.values.dIncludeTransparent, includeTransparent);
}
if (this.renderable.values.dIllumination.ref.value !== illuminationMode) {
needsUpdateSsao = true;
ValueCell.update(this.renderable.values.dIllumination, illuminationMode);
}
if (this.nSamples !== props.samples) {
needsUpdateSsao = true;
@@ -270,6 +334,7 @@ export class SsaoPass {
const multiScale = props.multiScale.name === 'on';
if (this.renderable.values.dMultiScale.ref.value !== multiScale) {
needsUpdateSsao = true;
ValueCell.update(this.renderable.values.dMultiScale, multiScale);
}
@@ -313,28 +378,45 @@ export class SsaoPass {
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.ssaoDepthTransparentTexture.define(sw, sh);
this.depthBlurProxyTexture.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);
this.downsampledDepthTargetOpaque.setSize(sw, sh);
this.depthHalfTargetOpaque.setSize(hw, hh);
this.depthQuarterTargetOpaque.setSize(qw, qh);
const depthTexture = this.getDepthTexture();
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
ValueCell.update(this.depthHalfRenderableOpaque.values.tColor, depthTexture);
ValueCell.update(this.renderable.values.tDepth, depthTexture);
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTarget.texture);
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTargetOpaque.texture);
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTargetOpaque.texture);
ValueCell.update(this.downsampleDepthRenderableOpaque.values.uTexSize, Vec2.set(this.downsampleDepthRenderableOpaque.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderableOpaque.values.uTexSize, Vec2.set(this.depthHalfRenderableOpaque.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderableOpaque.values.uTexSize, Vec2.set(this.depthQuarterRenderableOpaque.values.uTexSize.ref.value, qw, qh));
this.downsampledDepthTargetTransparent.setSize(sw, sh);
this.depthHalfTargetTransparent.setSize(hw, hh);
this.depthQuarterTargetTransparent.setSize(qw, qh);
const transparentDepthTexture = this.getTransparentDepthTexture();
ValueCell.update(this.depthHalfRenderableTransparent.values.tColor, transparentDepthTexture);
ValueCell.update(this.renderable.values.tDepthTransparent, transparentDepthTexture);
ValueCell.update(this.renderable.values.tDepthHalfTransparent, this.depthHalfTargetTransparent.texture);
ValueCell.update(this.renderable.values.tDepthQuarterTransparent, this.depthQuarterTargetTransparent.texture);
ValueCell.update(this.downsampleDepthRenderableTransparent.values.uTexSize, Vec2.set(this.downsampleDepthRenderableTransparent.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.depthHalfRenderableTransparent.values.uTexSize, Vec2.set(this.depthHalfRenderableTransparent.values.uTexSize.ref.value, hw, hh));
ValueCell.update(this.depthQuarterRenderableTransparent.values.uTexSize, Vec2.set(this.depthQuarterRenderableTransparent.values.uTexSize.ref.value, 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.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
@@ -350,16 +432,20 @@ export class SsaoPass {
}
if (needsUpdateDepthHalf) {
this.depthHalfRenderable.update();
this.depthHalfRenderableOpaque.update();
this.depthHalfRenderableTransparent.update();
}
}
render(camera: ICamera) {
if (isTimingMode) this.webgl.timer.mark('SsaoPass.render');
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
const { state } = this.webgl;
const { x, y, width, height } = camera.viewport;
const includeTransparent = this.renderable.values.dIncludeTransparent.ref.value;
const multiScale = this.renderable.values.dMultiScale.ref.value;
const sx = Math.floor(x * this.ssaoScale);
const sy = Math.floor(y * this.ssaoScale);
const sw = Math.ceil(width * this.ssaoScale);
@@ -369,31 +455,75 @@ export class SsaoPass {
state.scissor(sx, sy, sw, sh);
if (this.ssaoScale < 1) {
if (isTimingMode) this.webgl.timer.mark('SsaoPass.downsample');
this.downsampledDepthTarget.bind();
this.downsampleDepthRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.downsample');
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
this.downsampledDepthTargetOpaque.bind();
this.downsampleDepthRenderableOpaque.render();
if (includeTransparent) {
this.downsampledDepthTargetTransparent.bind();
this.downsampleDepthRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
}
if (isTimingMode) this.webgl.timer.mark('SsaoPass.half');
this.depthHalfTarget.bind();
this.depthHalfRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.half');
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
if (multiScale) {
this.depthHalfTargetOpaque.bind();
this.depthHalfRenderableOpaque.render();
}
if (multiScale && includeTransparent) {
this.depthHalfTargetTransparent.bind();
this.depthHalfRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
if (isTimingMode) this.webgl.timer.mark('SsaoPass.quarter');
this.depthQuarterTarget.bind();
this.depthQuarterRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.quarter');
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
if (multiScale) {
this.depthQuarterTargetOpaque.bind();
this.depthQuarterRenderableOpaque.render();
}
if (multiScale && includeTransparent) {
this.depthQuarterTargetTransparent.bind();
this.depthQuarterRenderableTransparent.render();
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
if (isTimingMode) this.webgl.timer.mark('SSAO.opaque');
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
ValueCell.update(this.renderable.values.uTransparencyFlag, 0);
this.framebuffer.bind();
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.opaque');
if (isTimingMode) this.webgl.timer.mark('SSAO.blurOpaque');
ValueCell.update(this.blurFirstPassRenderable.values.tSsaoDepth, this.ssaoDepthTexture);
this.blurFirstPassRenderable.update();
this.blurFirstPassFramebuffer.bind();
this.blurFirstPassRenderable.render();
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
this.blurSecondPassFramebuffer.bind();
this.blurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.render');
if (isTimingMode) this.webgl.timer.markEnd('SSAO.blurOpaque');
if (includeTransparent) {
if (isTimingMode) this.webgl.timer.mark('SSAO.transparent ');
this.ssaoDepthTransparentTexture.attachFramebuffer(this.framebuffer, 'color0');
ValueCell.update(this.renderable.values.uTransparencyFlag, 1);
this.framebuffer.bind();
this.renderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.transparent ');
if (isTimingMode) this.webgl.timer.mark('SSAO.blurTransparent ');
ValueCell.update(this.blurFirstPassRenderable.values.tSsaoDepth, this.ssaoDepthTransparentTexture);
this.blurFirstPassRenderable.update();
this.blurFirstPassFramebuffer.bind();
this.blurFirstPassRenderable.render();
this.ssaoDepthTransparentTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
this.blurSecondPassFramebuffer.bind();
this.blurSecondPassRenderable.render();
if (isTimingMode) this.webgl.timer.markEnd('SSAO.blurTransparent ');
}
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
}
}
@@ -403,6 +533,13 @@ const SsaoSchema = {
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
dIllumination: DefineSpec('boolean'),
uTransparencyFlag: UniformSpec('i'),
dIncludeTransparent: DefineSpec('boolean'),
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthHalfTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
tDepthQuarterTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
uSamples: UniformSpec('v3[]'),
dNSamples: DefineSpec('number'),
@@ -425,13 +562,20 @@ const SsaoSchema = {
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture, transparentDepthTexture: Texture, transparentDepthHalfTexture: Texture, transparentDepthQuarterTexture: Texture): SsaoRenderable {
const values: Values<typeof SsaoSchema> = {
...QuadValues,
tDepth: ValueCell.create(depthTexture),
tDepthHalf: ValueCell.create(depthHalfTexture),
tDepthQuarter: ValueCell.create(depthQuarterTexture),
dIllumination: ValueCell.create(false),
dIncludeTransparent: ValueCell.create(true),
uTransparencyFlag: ValueCell.create(0),
tDepthTransparent: ValueCell.create(transparentDepthTexture),
tDepthHalfTransparent: ValueCell.create(transparentDepthHalfTexture),
tDepthQuarterTransparent: ValueCell.create(transparentDepthQuarterTexture),
uSamples: ValueCell.create(getSamples(32)),
dNSamples: ValueCell.create(32),

View File

@@ -30,6 +30,7 @@ import { Helper } from '../helper/helper';
import { accumulate_frag } from '../../mol-gl/shader/illumination/accumulate.frag';
import { now } from '../../mol-util/now';
import { clamp } from '../../mol-math/interpolate';
import { DrawPass } from './draw';
type RenderContext = {
renderer: Renderer;
@@ -63,7 +64,6 @@ export class TracingPass {
readonly colorTextureOpaque: Texture;
readonly normalTextureOpaque: Texture;
readonly shadedTextureOpaque: Texture;
readonly depthTextureOpaque: Texture;
private readonly thicknessTarget: RenderTarget;
private readonly holdTarget: RenderTarget;
@@ -73,9 +73,13 @@ export class TracingPass {
private readonly traceRenderable: TraceRenderable;
private readonly accumulateRenderable: AccumulateRenderable;
constructor(private readonly webgl: WebGLContext, width: number, height: number) {
constructor(private readonly webgl: WebGLContext, private readonly drawPass: DrawPass) {
const { extensions: { drawBuffers, colorBufferHalfFloat, textureHalfFloat }, resources, isWebGL2 } = webgl;
const { depthTextureOpaque } = drawPass;
const width = depthTextureOpaque.getWidth();
const height = depthTextureOpaque.getHeight();
if (isWebGL2) {
this.shadedTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
this.shadedTextureOpaque.define(width, height);
@@ -100,9 +104,6 @@ export class TracingPass {
this.colorTextureOpaque.define(width, height);
}
this.depthTextureOpaque = resources.texture('image-depth', 'depth', isWebGL2 ? 'float' : 'ushort', 'nearest');
this.depthTextureOpaque.define(width, height);
this.framebuffer = resources.framebuffer();
this.framebuffer.bind();
@@ -115,14 +116,13 @@ export class TracingPass {
this.shadedTextureOpaque.attachFramebuffer(this.framebuffer, 'color0');
this.normalTextureOpaque.attachFramebuffer(this.framebuffer, 'color1');
this.colorTextureOpaque.attachFramebuffer(this.framebuffer, 'color2');
this.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
this.thicknessTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'nearest');
this.holdTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.accumulateTarget = webgl.createRenderTarget(width, height, false, 'float32');
this.composeTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
this.traceRenderable = getTraceRenderable(webgl, this.colorTextureOpaque, this.normalTextureOpaque, this.shadedTextureOpaque, this.thicknessTarget.texture, this.accumulateTarget.texture, this.depthTextureOpaque);
this.traceRenderable = getTraceRenderable(webgl, this.colorTextureOpaque, this.normalTextureOpaque, this.shadedTextureOpaque, this.thicknessTarget.texture, this.accumulateTarget.texture, this.drawPass.depthTextureOpaque);
this.accumulateRenderable = getAccumulateRenderable(webgl, this.holdTarget.texture);
}
@@ -131,9 +131,9 @@ export class TracingPass {
const { gl, state } = this.webgl;
this.framebuffer.bind();
this.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
this.drawPass.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
renderer.clear(true);
renderer.renderTracing(scene.primitives, camera, null);
renderer.renderTracing(scene.primitives, camera);
//
@@ -141,7 +141,7 @@ export class TracingPass {
this.thicknessTarget.bind();
state.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
renderer.renderDepthOpaqueBack(scene.primitives, camera, null);
renderer.renderDepthOpaqueBack(scene.primitives, camera);
}
if (isTimingMode) this.webgl.timer.markEnd('TracePass.renderInput');
}
@@ -159,7 +159,6 @@ export class TracingPass {
this.colorTextureOpaque.define(width, height);
this.normalTextureOpaque.define(width, height);
this.shadedTextureOpaque.define(width, height);
this.depthTextureOpaque.define(width, height);
ValueCell.update(this.traceRenderable.values.uTexSize, Vec2.set(this.traceRenderable.values.uTexSize.ref.value, width, height));
ValueCell.update(this.accumulateRenderable.values.uTexSize, Vec2.set(this.accumulateRenderable.values.uTexSize.ref.value, width, height));

View File

@@ -51,6 +51,12 @@ describe('sortedArray', () => {
test('predIndex4', SortedArray.findPredecessorIndex(a2468, 3), 1);
test('predIndexInt', SortedArray.findPredecessorIndexInInterval(a1234, 0, Interval.ofRange(2, 3)), 2);
const aDuplSmall = SortedArray.ofSortedArray([1, ...new Array(2).fill(3), 3]);
test('predIndexDuplSmall', SortedArray.findPredecessorIndex(aDuplSmall, 2), 1);
const aDuplBig = SortedArray.ofSortedArray([1, ...new Array(333).fill(2), ...new Array(666).fill(3), 4]);
test('predIndexDuplBig', SortedArray.findPredecessorIndex(aDuplBig, 3), 334);
testI('findRange', SortedArray.findRange(a2468, 2, 4), Interval.ofRange(0, 1));
it('deduplicate', () => {

View File

@@ -1,7 +1,8 @@
/**
* 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 David Sehnal <david.sehnal@gmail.com>
* @author Adam Midlik <midlik@gmail.com>
*/
import { sortArray, hash3, hash4, createRangeArray } from '../../util';
@@ -71,25 +72,20 @@ export function areEqual(a: Nums, b: Nums) {
}
/**
* Returns 0 if `v` is smaller or equal the first element of `xs`
* Returns length of `xs` if `v` is bigger than the last element of `xs`
* Otherwise returns the first index where the value of `xs` is equal or bigger than `v`
* Returns 0 if `query` is smaller or equal the first element of `xs`.
* Returns length of `xs` if `query` is bigger than the last element of `xs`.
* Otherwise returns the first index where the value of `xs` is equal or bigger than `query`.
*/
export function findPredecessorIndex(xs: Nums, v: number) {
const len = xs.length;
if (v <= xs[0]) return 0;
if (v > xs[len - 1]) return len;
return binarySearchPredIndexRange(xs, v, 0, len);
export function findPredecessorIndex(xs: Nums, query: number) {
return binarySearchPredIndexRange(xs, query, 0, xs.length);
}
export function findPredecessorIndexInInterval(xs: Nums, v: number, bounds: Interval) {
const s = Interval.start(bounds), e = Interval.end(bounds);
const sv = xs[s];
if (v <= sv) return s;
if (e > s && v > xs[e - 1]) return e;
// do a linear search if there are only 10 or less items remaining
if (v - sv <= 11) return linearSearchPredInRange(xs, v, s + 1, e);
return binarySearchPredIndexRange(xs, v, s, e);
/**
* Return index of the first element of `xs` within range `bounds` which is greater than or equal to `query`.
* Return end of `bounds` (exclusive) if all elements in the range are less than `query`.
*/
export function findPredecessorIndexInInterval(xs: Nums, query: number, bounds: Interval) {
return binarySearchPredIndexRange(xs, query, Interval.start(bounds), Interval.end(bounds));
}
export function findRange(xs: Nums, min: number, max: number) {
@@ -116,31 +112,29 @@ function binarySearchRange(xs: Nums, value: number, start: number, end: number)
return -1;
}
function binarySearchPredIndexRange(xs: Nums, value: number, start: number, end: number) {
let min = start, max = end - 1;
while (min < max) {
// do a linear search if there are only 10 or less items remaining
if (min + 11 > max) {
for (let i = min; i <= max; i++) {
if (value <= xs[i]) return i;
}
return max + 1;
}
/** Return index of the first element within range [start, end) which is greater than or equal to `query`.
* Return `end` if all elements in the range are less than `query`. */
function binarySearchPredIndexRange(xs: Nums, query: number, start: number, end: number): number {
if (start === end) return start;
if (xs[start] >= query) return start;
if (xs[end - 1] < query) return end;
// Invariants: xs[i] < query for each i < min, xs[i] >= query for each i >= max
let min = start, max = end;
while (max - min > 4) {
const mid = (min + max) >> 1;
const v = xs[mid];
if (value < v) max = mid - 1;
else if (value > v) min = mid + 1;
else return mid;
if (xs[mid] >= query) {
max = mid;
} else {
min = mid + 1;
}
}
if (min > max) return max + 1;
return xs[min] >= value ? min : min + 1;
}
function linearSearchPredInRange(xs: Nums, value: number, start: number, end: number) {
for (let i = start; i < end; i++) {
if (value <= xs[i]) return i;
// Linear search remaining elements:
for (let i = min; i < max; i++) {
if (xs[i] >= query) {
return i;
}
}
return end;
return max;
}
export function areIntersecting(a: Nums, b: Nums) {

View File

@@ -49,6 +49,7 @@ export interface DirectVolume {
readonly cartnToUnit: ValueCell<Mat4>
readonly packedGroup: ValueCell<boolean>
readonly axisOrder: ValueCell<Vec3>
readonly dataType: ValueCell<'byte' | 'float' | 'halfFloat'>
/** Bounding sphere of the volume */
readonly boundingSphere: Sphere3D
@@ -57,10 +58,10 @@ export interface DirectVolume {
}
export namespace DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume?: DirectVolume): DirectVolume {
export function create(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume?: DirectVolume): DirectVolume {
return directVolume ?
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder);
update(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume) :
fromData(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType);
}
function hashCode(directVolume: DirectVolume) {
@@ -71,7 +72,7 @@ export namespace DirectVolume {
]);
}
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3): DirectVolume {
function fromData(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat'): DirectVolume {
const boundingSphere = Sphere3D();
let currentHash = -1;
@@ -103,6 +104,7 @@ export namespace DirectVolume {
},
packedGroup: ValueCell.create(packedGroup),
axisOrder: ValueCell.create(axisOrder),
dataType: ValueCell.create(dataType),
setBoundingSphere(sphere: Sphere3D) {
Sphere3D.copy(boundingSphere, sphere);
currentHash = hashCode(directVolume);
@@ -111,7 +113,7 @@ export namespace DirectVolume {
return directVolume;
}
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, directVolume: DirectVolume): DirectVolume {
function update(bbox: Box3D, gridDimension: Vec3, transform: Mat4, unitToCartn: Mat4, cellDim: Vec3, texture: Texture, stats: Grid['stats'], packedGroup: boolean, axisOrder: Vec3, dataType: 'byte' | 'float' | 'halfFloat', directVolume: DirectVolume): DirectVolume {
const width = texture.getWidth();
const height = texture.getHeight();
const depth = texture.getDepth();
@@ -129,6 +131,7 @@ export namespace DirectVolume {
ValueCell.update(directVolume.cartnToUnit, Mat4.invert(Mat4(), unitToCartn));
ValueCell.updateIfChanged(directVolume.packedGroup, packedGroup);
ValueCell.updateIfChanged(directVolume.axisOrder, Vec3.fromArray(directVolume.axisOrder.ref.value, axisOrder, 0));
ValueCell.updateIfChanged(directVolume.dataType, dataType);
return directVolume;
}
@@ -142,7 +145,8 @@ export namespace DirectVolume {
const stats = Grid.One.stats;
const packedGroup = false;
const axisOrder = Vec3.create(0, 1, 2);
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, directVolume);
const dataType = 'byte';
return create(bbox, gridDimension, transform, unitToCartn, cellDim, texture, stats, packedGroup, axisOrder, dataType, directVolume);
}
export const Params = {

View File

@@ -66,7 +66,10 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
const q1 = Math.round(radialSegments / 4);
const q3 = q1 * 3;
const roundCapFlag = roundCap && linearSegments && !(startCap && endCap) && (startCap || endCap); // disabled if both caps are active
const roundCapFlag = roundCap && linearSegments && (startCap || endCap);
let halfLinearSegments;
const doubleRoundCap = roundCapFlag && startCap && endCap;
if (doubleRoundCap) halfLinearSegments = linearSegments / 2;
for (let i = 0; i <= linearSegments; ++i) {
const i3 = i * 3;
v3fromArray(u, normalVectors, i3);
@@ -77,10 +80,15 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
let height = heightValues[i];
let capSmoothingFactor: number;
if (roundCapFlag) {
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((startCap ? linearSegments - i : i) / (linearSegments), 2)));
const sc = doubleRoundCap ? i <= halfLinearSegments! : startCap;
if (doubleRoundCap) {
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((sc ? halfLinearSegments! - i : i - halfLinearSegments!) / halfLinearSegments!, 2)));
} else {
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((sc ? linearSegments - i : i) / linearSegments, 2)));
}
width *= capSmoothingFactor;
height *= capSmoothingFactor;
v3cross(capNormalSmoothingVector, startCap ? v : u, startCap ? u : v);
v3cross(capNormalSmoothingVector, sc ? v : u, sc ? u : v);
v3normalize(capNormalSmoothingVector, capNormalSmoothingVector);
}
const rounded = crossSection === 'rounded' && height > width;

View File

@@ -285,7 +285,7 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
webgl.namedTextures[ColorCountName] = resources.texture('image-float32', 'alpha', 'float', 'nearest');
}
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
// this is quite wasteful but good enough for medium size meshes
if (!webgl.namedTextures[ColorAccumulateName]) {

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>
*/
@@ -16,6 +16,7 @@ export type TransparencyData = {
uTransparencyTexDim: ValueCell<Vec2>
dTransparency: ValueCell<boolean>,
transparencyAverage: ValueCell<number>,
transparencyMin: ValueCell<number>,
tTransparencyGrid: ValueCell<Texture>,
uTransparencyGridDim: ValueCell<Vec3>,
@@ -40,6 +41,16 @@ export function getTransparencyAverage(array: Uint8Array, count: number): number
return sum / (255 * count);
}
/** exclude fully opaque parts */
export function getTransparencyMin(array: Uint8Array, count: number): number {
if (count === 0 || array.length < count) return 1;
let min = 255;
for (let i = 0; i < count; ++i) {
if (array[i] > 0 && array[i] < min) min = array[i];
}
return min / 255;
}
export function clearTransparency(array: Uint8Array, start: number, end: number) {
array.fill(0, start, end);
}
@@ -51,6 +62,7 @@ export function createTransparency(count: number, type: TransparencyType, transp
ValueCell.update(transparencyData.uTransparencyTexDim, Vec2.create(transparency.width, transparency.height));
ValueCell.updateIfChanged(transparencyData.dTransparency, count > 0);
ValueCell.updateIfChanged(transparencyData.transparencyAverage, getTransparencyAverage(transparency.array, count));
ValueCell.updateIfChanged(transparencyData.transparencyMin, getTransparencyMin(transparency.array, count));
ValueCell.updateIfChanged(transparencyData.dTransparencyType, type);
return transparencyData;
} else {
@@ -59,6 +71,7 @@ export function createTransparency(count: number, type: TransparencyType, transp
uTransparencyTexDim: ValueCell.create(Vec2.create(transparency.width, transparency.height)),
dTransparency: ValueCell.create(count > 0),
transparencyAverage: ValueCell.create(0),
transparencyMin: ValueCell.create(1),
tTransparencyGrid: ValueCell.create(createNullTexture()),
uTransparencyGridDim: ValueCell.create(Vec3.create(1, 1, 1)),
@@ -81,6 +94,7 @@ export function createEmptyTransparency(transparencyData?: TransparencyData): Tr
uTransparencyTexDim: ValueCell.create(Vec2.create(1, 1)),
dTransparency: ValueCell.create(false),
transparencyAverage: ValueCell.create(0),
transparencyMin: ValueCell.create(1),
tTransparencyGrid: ValueCell.create(createNullTexture()),
uTransparencyGridDim: ValueCell.create(Vec3.create(1, 1, 1)),

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>
*/
@@ -7,7 +7,7 @@
import { ComputeRenderable, createComputeRenderable } from '../../renderable';
import { WebGLContext } from '../../webgl/context';
import { createComputeRenderItem } from '../../webgl/render-item';
import { Values, TextureSpec, UniformSpec } from '../../renderable/schema';
import { Values, TextureSpec, UniformSpec, DefineSpec } from '../../renderable/schema';
import { Texture } from '../../../mol-gl/webgl/texture';
import { ShaderCode } from '../../../mol-gl/shader-code';
import { ValueCell } from '../../../mol-util';
@@ -17,12 +17,14 @@ import { getTriCount } from './tables';
import { quad_vert } from '../../../mol-gl/shader/quad.vert';
import { activeVoxels_frag } from '../../../mol-gl/shader/marching-cubes/active-voxels.frag';
import { isTimingMode } from '../../../mol-util/debug';
import { isWebGL2 } from '../../webgl/compat';
const ActiveVoxelsSchema = {
...QuadSchema,
tTriCount: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uGridDim: UniformSpec('v3'),
@@ -34,12 +36,17 @@ type ActiveVoxelsValues = Values<typeof ActiveVoxelsSchema>
const ActiveVoxelsName = 'active-voxels';
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, scale: Vec2): ComputeRenderable<ActiveVoxelsValues> {
if (ctx.namedComputeRenderables[ActiveVoxelsName]) {
const v = ctx.namedComputeRenderables[ActiveVoxelsName].values as ActiveVoxelsValues;
ValueCell.update(v.uQuadScale, scale);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.update(v.uGridDim, gridDim);
ValueCell.update(v.uGridTexDim, gridTexDim);
@@ -59,6 +66,7 @@ function createActiveVoxelsRenderable(ctx: WebGLContext, volumeData: Texture, gr
uQuadScale: ValueCell.create(scale),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uGridDim: ValueCell.create(gridDim),
uGridTexDim: ValueCell.create(gridTexDim),

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>
*/
@@ -28,6 +28,7 @@ const IsosurfaceSchema = {
tActiveVoxelsPyramid: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tActiveVoxelsBase: TextureSpec('texture', 'rgba', 'float', 'nearest'),
tVolumeData: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
dValueChannel: DefineSpec('string', ['red', 'alpha']),
uIsoValue: UniformSpec('f'),
uSize: UniformSpec('f'),
@@ -48,6 +49,10 @@ type IsosurfaceValues = Values<typeof IsosurfaceSchema>
const IsosurfaceName = 'isosurface';
function valueChannel(ctx: WebGLContext, volumeData: Texture) {
return isWebGL2(ctx.gl) && volumeData.format === ctx.gl.RED ? 'red' : 'alpha';
}
function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
if (ctx.namedComputeRenderables[IsosurfaceName]) {
const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
@@ -55,6 +60,7 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
ValueCell.update(v.tActiveVoxelsPyramid, activeVoxelsPyramid);
ValueCell.update(v.tActiveVoxelsBase, activeVoxelsBase);
ValueCell.update(v.tVolumeData, volumeData);
ValueCell.update(v.dValueChannel, valueChannel(ctx, volumeData));
ValueCell.updateIfChanged(v.uIsoValue, isoValue);
ValueCell.updateIfChanged(v.uSize, Math.pow(2, levels));
@@ -87,6 +93,7 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
tActiveVoxelsPyramid: ValueCell.create(activeVoxelsPyramid),
tActiveVoxelsBase: ValueCell.create(activeVoxelsBase),
tVolumeData: ValueCell.create(volumeData),
dValueChannel: ValueCell.create(valueChannel(ctx, volumeData)),
uIsoValue: ValueCell.create(isoValue),
uSize: ValueCell.create(Math.pow(2, levels)),
@@ -155,7 +162,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
}
} else {
// in webgl1 drawbuffers must be in the same format for some reason
// webgl1 requires consistent bit plane counts
// this is quite wasteful but good enough for medium size meshes
if (!vertexTexture) {

View File

@@ -253,6 +253,7 @@ export const TransparencySchema = {
tTransparency: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
dTransparency: DefineSpec('boolean'),
transparencyAverage: ValueSpec('number'),
transparencyMin: ValueSpec('number'),
uTransparencyGridDim: UniformSpec('v3'),
uTransparencyGridTransform: UniformSpec('v4'),

View File

@@ -60,28 +60,25 @@ interface Renderer {
readonly light: Readonly<Light>
readonly ambientColor: Vec3
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => void
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => void
clearDepth: (packed?: boolean) => void
update: (camera: ICamera, scene: Scene) => void
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', depthTexture: Texture | null, pickType: PickType) => void
renderDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthOpaqueBack: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderPick: (group: Scene.Group, camera: ICamera, variant: 'pick' | 'depth', pickType: PickType) => void
renderDepth: (group: Scene.Group, camera: ICamera) => void
renderDepthOpaque: (group: Scene.Group, camera: ICamera) => void
renderDepthOpaqueBack: (group: Scene.Group, camera: ICamera) => void
renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture) => void
renderMarkingDepth: (group: Scene.Group, camera: ICamera) => void
renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderEmissive: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderTracing: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderEmissive: (group: Scene.Group, camera: ICamera) => void
renderTracing: (group: Scene.Group, camera: ICamera) => void
renderBlended: (group: Scene, camera: ICamera) => void
renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderWboitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderWboitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDpoitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderDpoitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => void
renderDpoitVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
renderOpaque: (group: Scene.Group, camera: ICamera) => void
renderBlendedTransparent: (group: Scene.Group, camera: ICamera) => void
renderVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture) => void
renderWboitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture) => void
renderDpoitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => void
setProps: (props: Partial<RendererProps>) => void
setViewport: (x: number, y: number, width: number, height: number) => void
@@ -160,7 +157,6 @@ namespace Renderer {
None = 0,
BlendedFront = 1,
BlendedBack = 2,
BlendedVolume = 3,
}
const enum Mask {
@@ -327,12 +323,6 @@ namespace Renderer {
// culling done in fragment shader
state.disable(gl.CULL_FACE);
state.frontFace(gl.CCW);
if (flag === Flag.BlendedVolume) {
// depth test done manually in shader against `depthTexture`
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
}
} else if (flag === Flag.BlendedFront) {
state.enable(gl.CULL_FACE);
if (r.values.dFlipSided?.ref.value) {
@@ -456,13 +446,13 @@ namespace Renderer {
);
};
const renderPick = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, depthTexture: Texture | null, pickType: PickType) => {
const renderPick = (group: Scene.Group, camera: ICamera, variant: GraphicsRenderVariant, pickType: PickType) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderPick');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.All, false);
updateInternal(group, camera, null, Mask.All, false);
ValueCell.updateIfChanged(globalUniforms.uPickType, pickType);
const { renderables } = group;
@@ -474,13 +464,13 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderPick');
};
const renderDepth = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderDepth = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepth');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.All, false);
updateInternal(group, camera, null, Mask.All, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -489,13 +479,13 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepth');
};
const renderDepthOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderDepthOpaque = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepthOpaque');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
updateInternal(group, camera, null, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -507,14 +497,14 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthOpaque');
};
const renderDepthOpaqueBack = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderDepthOpaqueBack = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepthOpaqueBack');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
state.depthFunc(gl.GREATER);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
updateInternal(group, camera, null, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -527,7 +517,7 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthOpaqueBack');
};
const renderDepthTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderDepthTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDepthTransparent');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
@@ -545,13 +535,13 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDepthTransparent');
};
const renderMarkingDepth = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderMarkingDepth = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderMarkingDepth');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.All, false);
updateInternal(group, camera, null, Mask.All, false);
ValueCell.updateIfChanged(globalUniforms.uMarkingType, MarkingType.Depth);
const { renderables } = group;
@@ -586,13 +576,13 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
};
const renderEmissive = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderEmissive = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderEmissive');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
updateInternal(group, camera, null, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -604,13 +594,13 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderEmissive');
};
const renderTracing = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderTracing = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderTracing');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
updateInternal(group, camera, null, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -624,20 +614,20 @@ namespace Renderer {
const renderBlended = (scene: Scene, camera: ICamera) => {
if (scene.hasOpaque) {
renderBlendedOpaque(scene, camera, null);
renderOpaque(scene, camera);
}
if (scene.opacityAverage < 1) {
renderBlendedTransparent(scene, camera, null);
renderBlendedTransparent(scene, camera);
}
};
const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedOpaque');
const renderOpaque = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderOpaque');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
updateInternal(group, camera, null, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -646,10 +636,10 @@ namespace Renderer {
renderObject(r, 'color', Flag.None);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedOpaque');
if (isTimingMode) ctx.timer.markEnd('Renderer.renderOpaque');
};
const renderBlendedTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
const renderBlendedTransparent = (group: Scene.Group, camera: ICamera) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedTransparent');
if (transparentBackground) {
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
@@ -660,7 +650,7 @@ namespace Renderer {
state.enable(gl.DEPTH_TEST);
state.depthMask(false);
updateInternal(group, camera, depthTexture, Mask.Transparent, false);
updateInternal(group, camera, null, Mask.Transparent, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
@@ -680,10 +670,12 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedTransparent');
};
const renderBlendedVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderBlendedVolume');
const renderVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderVolume');
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
state.enable(gl.BLEND);
// depth test done manually in shader against `depthTexture`
state.disable(gl.DEPTH_TEST);
state.depthMask(false);
updateInternal(group, camera, depthTexture, Mask.Transparent, false);
@@ -692,28 +684,10 @@ namespace Renderer {
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (r.values.dGeometryType.ref.value === 'directVolume') {
renderObject(r, 'color', Flag.BlendedVolume);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderBlendedVolume');
};
const renderWboitOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderWboitOpaque');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (checkOpaque(r)) {
renderObject(r, 'color', Flag.None);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderWboitOpaque');
if (isTimingMode) ctx.timer.markEnd('Renderer.renderVolume');
};
const renderWboitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
@@ -730,25 +704,7 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderWboitTransparent');
};
const renderDpoitOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitOpaque');
state.disable(gl.BLEND);
state.enable(gl.DEPTH_TEST);
state.depthMask(true);
updateInternal(group, camera, depthTexture, Mask.Opaque, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (checkOpaque(r)) {
renderObject(r, 'color', Flag.None);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitOpaque');
};
const renderDpoitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => {
const renderDpoitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitTransparent');
state.enable(gl.BLEND);
@@ -770,31 +726,14 @@ namespace Renderer {
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitTransparent');
};
const renderDpoitVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitVolume');
state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
state.enable(gl.BLEND);
updateInternal(group, camera, depthTexture, Mask.Transparent, false);
const { renderables } = group;
for (let i = 0, il = renderables.length; i < il; ++i) {
const r = renderables[i];
if (r.values.dGeometryType.ref.value === 'directVolume') {
renderObject(r, 'color', Flag.None);
}
}
if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitVolume');
};
return {
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => {
clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean, forceToTransparency?: boolean) => {
state.enable(gl.SCISSOR_TEST);
state.enable(gl.DEPTH_TEST);
state.colorMask(true, true, true, true);
state.depthMask(true);
if (transparentBackground && !ignoreTransparentBackground) {
if (forceToTransparency || transparentBackground && !ignoreTransparentBackground) {
state.clearColor(0, 0, 0, 0);
} else if (toBackgroundColor) {
state.clearColor(bgColor[0], bgColor[1], bgColor[2], 1);
@@ -828,14 +767,11 @@ namespace Renderer {
renderEmissive,
renderTracing,
renderBlended,
renderBlendedOpaque,
renderOpaque,
renderBlendedTransparent,
renderBlendedVolume,
renderWboitOpaque,
renderVolume,
renderWboitTransparent,
renderDpoitOpaque,
renderDpoitTransparent,
renderDpoitVolume,
setProps: (props: Partial<RendererProps>) => {
if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {

View File

@@ -88,6 +88,8 @@ interface Scene extends Object3D {
readonly emissiveAverage: number
/** Opacity average of primitive renderables */
readonly opacityAverage: number
/** Transparency minimum, excluding fully opaque, of primitive renderables */
readonly transparencyMin: number
/** Is `true` if any primitive renderable (possibly) has any opaque part */
readonly hasOpaque: boolean
}
@@ -112,11 +114,13 @@ namespace Scene {
let markerAverageDirty = true;
let emissiveAverageDirty = true;
let opacityAverageDirty = true;
let transparencyMinDirty = true;
let hasOpaqueDirty = true;
let markerAverage = 0;
let emissiveAverage = 0;
let opacityAverage = 0;
let transparencyMin = 0;
let hasOpaque = false;
const object3d = Object3D.create();
@@ -176,6 +180,7 @@ namespace Scene {
markerAverageDirty = true;
emissiveAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
return true;
}
@@ -201,6 +206,7 @@ namespace Scene {
markerAverageDirty = true;
emissiveAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
visibleHash = newVisibleHash;
return true;
@@ -243,7 +249,7 @@ namespace Scene {
// TODO: simplify, handle in renderable.state???
// uAlpha is updated in "render" so we need to recompute it here
const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
const xray = p.values.dXrayShaded?.ref.value ? 0.5 : 1;
const xray = (p.values.dXrayShaded?.ref.value === 'on' || p.values.dXrayShaded?.ref.value === 'inverted') ? 0.5 : 1;
const fuzzy = p.values.dPointStyle?.ref.value === 'fuzzy' ? 0.5 : 1;
const text = p.values.dGeometryType.ref.value === 'text' ? 0.5 : 1;
opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text;
@@ -252,6 +258,28 @@ namespace Scene {
return count > 0 ? opacityAverage / count : 0;
}
/** exclude fully opaque parts */
function calculateTransparencyMin() {
if (primitives.length === 0) return 1;
let transparencyMin = 1;
const transparenyValues: number[] = [];
for (let i = 0, il = primitives.length; i < il; ++i) {
const p = primitives[i];
if (!p.state.visible) continue;
transparenyValues.length = 0;
const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
if (alpha < 1) transparenyValues.push(1 - alpha);
if (p.values.dXrayShaded?.ref.value === 'on' ||
p.values.dXrayShaded?.ref.value === 'inverted' ||
p.values.dPointStyle?.ref.value === 'fuzzy' ||
p.values.dGeometryType.ref.value === 'text'
) transparenyValues.push(0.5);
if (p.values.transparencyMin.ref.value > 0) transparenyValues.push(p.values.transparencyMin.ref.value);
transparencyMin = Math.min(transparencyMin, ...transparenyValues);
}
return transparencyMin;
}
function calculateHasOpaque() {
if (primitives.length === 0) return false;
for (let i = 0, il = primitives.length; i < il; ++i) {
@@ -299,6 +327,7 @@ namespace Scene {
markerAverageDirty = true;
emissiveAverageDirty = true;
opacityAverageDirty = true;
transparencyMinDirty = true;
hasOpaqueDirty = true;
},
add: (o: GraphicsRenderObject) => commitQueue.add(o),
@@ -361,6 +390,13 @@ namespace Scene {
}
return opacityAverage;
},
get transparencyMin() {
if (transparencyMinDirty) {
transparencyMin = calculateTransparencyMin();
transparencyMinDirty = false;
}
return transparencyMin;
},
get hasOpaque() {
if (hasOpaqueDirty) {
hasOpaque = calculateHasOpaque();

View File

@@ -325,8 +325,6 @@ const glsl300FragPrefixCommon = `
#define gl_FragColor out_FragData0
#define gl_FragDepthEXT gl_FragDepth
#define depthTextureSupport
`;
function getGlsl300VertPrefix(extensions: WebGLExtensions, shaderExtensions: ShaderExtensions) {
@@ -393,6 +391,9 @@ function getGlsl300FragPrefix(gl: WebGL2RenderingContext, extensions: WebGLExten
prefix.push('#define enabledShaderTextureLod');
}
}
if (extensions.depthTexture) {
prefix.push('#define depthTextureSupport');
}
prefix.push(glsl300FragPrefixCommon);
return prefix.join('\n') + '\n';
}

View File

@@ -13,7 +13,7 @@ if (uFog) {
gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
}
} else {
#if defined(dRenderVariant_colorDpoit)
#if defined(dRenderVariant_colorDpoit) && !defined(dGeometryType_directVolume)
if (gl_FragColor.a < 1.0) {
// transparent objects are blended with background color
gl_FragColor.a = fogAlpha;
@@ -28,5 +28,10 @@ if (uFog) {
gl_FragColor.a = fogAlpha;
#endif
}
} else if (uTransparentBackground) {
#if !defined(dRenderVariant_colorDpoit) && !defined(dGeometryType_directVolume)
// pre-multiplied alpha expected for transparent background
gl_FragColor.rgb *= gl_FragColor.a;
#endif
}
`;

View File

@@ -110,11 +110,5 @@ export const apply_light_color = `
gl_FragColor = vec4(outgoingLight, color.a);
#endif
#if defined(dXrayShaded_on)
gl_FragColor.a *= 1.0 - pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
#elif defined(dXrayShaded_inverted)
gl_FragColor.a *= pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
#endif
gl_FragColor.rgb *= uExposure;
`;

View File

@@ -34,35 +34,50 @@ export const assign_material_color = `
roughness = mix(roughness, vSubstance.g, sf);
bumpiness = mix(bumpiness, vSubstance.b, sf);
#endif
#if defined(dXrayShaded)
material.a = calcXrayShadedAlpha(material.a, normal);
#endif
#elif defined(dRenderVariant_depth)
if (fragmentDepth > getDepth(gl_FragCoord.xy / uDrawingBufferSize)) {
discard;
}
#ifndef dXrayShaded
vec4 material;
if (uRenderMask == MaskOpaque) {
#if defined(dXrayShaded)
discard;
#endif
#if defined(dTransparency)
float dta = 1.0 - vTransparency;
if (vTransparency < 0.2) dta = 1.0; // hard cutoff looks better
#if __VERSION__ == 100 || defined(dVaryingGroup)
if (vTransparency < 0.1) dta = 1.0; // hard cutoff to avoid artifacts
#endif
if (uRenderMask == MaskTransparent && uAlpha * dta == 1.0) {
discard;
} else if (uRenderMask == MaskOpaque && uAlpha * dta < 1.0) {
if (uAlpha * dta < 1.0) {
discard;
}
#else
if (uRenderMask == MaskTransparent && uAlpha == 1.0) {
discard;
} else if (uRenderMask == MaskOpaque && uAlpha < 1.0) {
if (uAlpha < 1.0) {
discard;
}
#endif
#else
if (uRenderMask == MaskOpaque) {
discard;
}
#endif
material = packDepthToRGBA(fragmentDepth);
} else if (uRenderMask == MaskTransparent) {
float alpha = uAlpha;
#if defined(dTransparency)
float dta = 1.0 - vTransparency;
alpha *= dta;
#endif
vec4 material = packDepthToRGBA(fragmentDepth);
#ifdef dXrayShaded
alpha = calcXrayShadedAlpha(alpha, normal);
#else
if (alpha == 1.0) {
discard;
}
#endif
material = packDepthWithAlphaToRGBA(fragmentDepth, alpha);
}
#elif defined(dRenderVariant_marking)
vec4 material;
if(uMarkingType == 1) {

View File

@@ -6,7 +6,7 @@ export const check_transparency = `
if (interior) material.a = 1.0;
#endif
#if !defined(dXrayShaded_on) && !defined(dXrayShaded_inverted)
#if !defined(dXrayShaded)
if ((uRenderMask == MaskOpaque && material.a < 1.0) ||
(uRenderMask == MaskTransparent && material.a == 1.0)
) {
@@ -14,4 +14,10 @@ export const check_transparency = `
}
#endif
#endif
#if defined(dRenderVariant_depth)
#if defined(dTransparentBackfaces_off)
if (interior) discard;
#endif
#endif
`;

View File

@@ -149,4 +149,15 @@ float fbm(in vec3 p) {
return f;
}
#ifdef dXrayShaded
float calcXrayShadedAlpha(in float alpha, const in vec3 normal) {
#if defined(dXrayShaded_on)
alpha *= 1.0 - pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
#elif defined(dXrayShaded_inverted)
alpha *= pow(abs(dot(normal, vec3(0.0, 0.0, 1.0))), uXrayEdgeFalloff);
#endif
return clamp(alpha, 0.001, 0.999);
}
#endif
`;

View File

@@ -25,6 +25,10 @@ export const common = `
#define dXrayShaded
#endif
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing) || (defined(dRenderVariant_depth) && defined(dXrayShaded))
#define dNeedsNormal
#endif
#define MaskAll 0
#define MaskOpaque 1
#define MaskTransparent 2
@@ -99,6 +103,15 @@ float unpackRGBAToDepth(const in vec4 v) {
return dot(v, UnpackFactors);
}
vec4 packDepthWithAlphaToRGBA(const in float depth, const in float alpha){
vec3 r = vec3(fract(depth * PackFactors.yz), depth);
r.yz -= r.xy * ShiftRight8; // tidy overflow
return vec4(r * PackUpscale, alpha);
}
vec2 unpackRGBAToDepthWithAlpha(const in vec4 v) {
return vec2(dot(v.xyz, UnpackFactors.yzw), v.w);
}
vec4 sRGBToLinear(const in vec4 c) {
return vec4(mix(pow(c.rgb * 0.9478672986 + vec3(0.0521327014), vec3(2.4)), c.rgb * 0.0773993808, vec3(lessThanEqual(c.rgb, vec3(0.04045)))), c.a);
}
@@ -128,6 +141,15 @@ float depthToViewZ(const in float isOrtho, const in float linearClipZ, const in
return isOrtho == 1.0 ? orthographicDepthToViewZ(linearClipZ, near, far) : perspectiveDepthToViewZ(linearClipZ, near, far);
}
// see https://github.com/graphitemaster/normals_revisited and https://www.shadertoy.com/view/3s33zj
mat3 adjoint(const in mat4 m) {
return mat3(
cross(m[1].xyz, m[2].xyz),
cross(m[2].xyz, m[0].xyz),
cross(m[0].xyz, m[1].xyz)
);
}
#if __VERSION__ == 100
// transpose

View File

@@ -248,6 +248,12 @@ void main() {
#include fade_lod
#include clip_pixel
#ifdef dNeedsNormal
mat3 normalMatrix = adjoint(uView);
vec3 normal = normalize(normalMatrix * -normalize(cameraNormal));
#endif
#include assign_material_color
#include check_transparency
@@ -268,8 +274,6 @@ void main() {
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
mat3 normalMatrix = transpose3(inverse3(mat3(uView)));
vec3 normal = normalize(normalMatrix * -normalize(cameraNormal));
#include apply_light_color
#include apply_interior_color
#include apply_marker_color

View File

@@ -1,24 +0,0 @@
export const depthMerge_frag = `
precision highp float;
precision highp sampler2D;
uniform sampler2D tDepthPrimitives;
uniform sampler2D tDepthVolumes;
uniform vec2 uTexSize;
#include common
float getDepth(const in vec2 coords, sampler2D tDepth) {
#ifdef dPackedDepth
return unpackRGBAToDepth(texture2D(tDepth, coords));
#else
return texture2D(tDepth, coords).r;
#endif
}
void main() {
vec2 coords = gl_FragCoord.xy / uTexSize;
float depth = min(getDepth(coords, tDepthPrimitives), getDepth(coords, tDepthVolumes));
gl_FragColor = packDepthToRGBA(depth);
}
`;

View File

@@ -167,7 +167,7 @@ vec3 v3m4(vec3 p, mat4 m) {
float preFogAlphaBlended = 0.0;
vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
mat3 normalMatrix = transpose3(inverse3(mat3(uModelView * vTransform)));
mat3 normalMatrix = adjoint(uModelView * vTransform);
mat4 cartnToUnit = uCartnToUnit * inverse4(vTransform);
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
mat4 modelTransform = uModel * vTransform * uTransform;
@@ -332,7 +332,7 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
src = gl_FragColor;
if (!uTransparentBackground) {
if (!uTransparentBackground || !uFog) {
// done in 'apply_fog' otherwise
src.rgb *= src.a;
}

View File

@@ -52,7 +52,7 @@ float getDepthOpaque(const in vec2 coords) {
// Retrieve depth from transparent depth texture
float getDepthTransparent(const in vec2 coords) {
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
}
bool isBackground(const in float depth) {

View File

@@ -5,6 +5,9 @@ precision highp sampler2D;
uniform sampler2D tShaded;
uniform sampler2D tColor;
uniform sampler2D tNormal;
uniform sampler2D tTransparentColor;
uniform sampler2D tSsaoDepth;
uniform sampler2D tSsaoDepthTransparent;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform sampler2D tOutlines;
@@ -16,6 +19,7 @@ uniform float uFogNear;
uniform float uFogFar;
uniform vec3 uFogColor;
uniform vec3 uOutlineColor;
uniform vec3 uOcclusionColor;
uniform bool uTransparentBackground;
uniform float uDenoiseThreshold;
@@ -39,8 +43,8 @@ float getDepthOpaque(const in vec2 coords) {
}
float getDepthTransparent(const in vec2 coords) {
#ifdef dTransparentOutline
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
#if defined(dTransparentOutline) || defined(dOcclusionEnable)
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
#else
return 1.0;
#endif
@@ -50,6 +54,28 @@ bool isBackground(const in float depth) {
return depth == 1.0;
}
float getSsao(vec2 coords) {
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepth, coords).xy);
if (rawSsao > 0.999) {
return 1.0;
} else if (rawSsao > 0.001) {
return rawSsao;
}
// treat values close to 0.0 as errors and return no occlusion
return 1.0;
}
float getSsaoTransparent(vec2 coords) {
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepthTransparent, coords).xy);
if (rawSsao > 0.999) {
return 1.0;
} else if (rawSsao > 0.001) {
return rawSsao;
}
// treat values close to 0.0 as errors and return no occlusion
return 1.0;
}
//
// TODO: investigate
@@ -117,20 +143,16 @@ vec4 smartDeNoise(sampler2D tex, vec2 uv) {
return aBuff / zBuff;
}
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
float backgroundViewZ = 2.0 * uFar;
int squaredOutlineScale = dOutlineScale * dOutlineScale;
float getOutline(const in vec2 coords, const in float opaqueDepth, const in float transparentDepth, out float closestTexel, out float isTransparent) {
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;
isTransparent = 0.0;
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
if (x * x + y * y > dOutlineScale * dOutlineScale) {
if (x * x + y * y > squaredOutlineScale) {
continue;
}
@@ -139,16 +161,15 @@ 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;
isTransparent = sampleOutlineCombined.a;
}
}
}
return closestTexel < opaqueDepth ? outline : 1.0;
return isTransparent == 0.0 ? outline : (closestTexel > opaqueDepth && closestTexel < transparentDepth) ? 1.0 : outline;
}
void main() {
@@ -161,12 +182,19 @@ void main() {
#endif
float opaqueDepth = getDepthOpaque(coords);
float backgroundViewZ = 2.0 * uFar;
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
float fogFactor = smoothstep(uFogNear, uFogFar, abs(opaqueSelfViewZ));
float fogAlpha = 1.0 - fogFactor;
float transparentDepth = 1.0;
#ifdef dBlendTransparency
bool blendTransparency = true;
vec4 transparentColor = texture2D(tTransparentColor, coords);
transparentDepth = getDepthTransparent(coords);
#endif
float alpha = 1.0;
if (!uTransparentBackground) {
// mix opaque objects with background color
@@ -177,17 +205,63 @@ void main() {
color.rgb *= fogAlpha;
}
#if defined(dOcclusionEnable)
if (!isBackground(opaqueDepth)) {
float occlusionFactor = getSsao(coords);
if (!uTransparentBackground) {
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
} else {
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
}
}
#ifdef dBlendTransparency
if (!isBackground(transparentDepth)) {
float viewDist = abs(getViewZ(transparentDepth));
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
float occlusionFactor = getSsaoTransparent(coords);
transparentColor.rgb = mix(uOcclusionColor * (1.0 - fogFactor), transparentColor.rgb, occlusionFactor);
}
#endif
#endif
#ifdef dOutlineEnable
float closestTexel;
float outline = getOutline(coords, opaqueDepth, closestTexel);
float isTransparentOutline;
float outline = getOutline(coords, opaqueDepth, transparentDepth, closestTexel, isTransparentOutline);
if (outline == 0.0) {
float viewDist = abs(getViewZ(closestTexel));
float fogFactorOutline = smoothstep(uFogNear, uFogFar, viewDist);
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
if (!uTransparentBackground) {
color.rgb = mix(uOutlineColor, uFogColor, fogFactorOutline);
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
} else {
color.rgb = mix(uOutlineColor, color.rgb, fogFactorOutline);
alpha = 1.0 - fogFactorOutline;
alpha = 1.0 - fogFactor;
color.rgb = mix(uOutlineColor, vec3(0.0), fogFactor);
}
#ifdef dBlendTransparency
if (isTransparentOutline == 1.0 || transparentDepth > closestTexel) {
blendTransparency = false;
}
#endif
}
#endif
#ifdef dBlendTransparency
if (blendTransparency) {
if (transparentColor.a != 0.0) {
if (isBackground(opaqueDepth)) {
if (uTransparentBackground) {
color = transparentColor;
alpha = transparentColor.a;
} else {
color.rgb = transparentColor.rgb + uFogColor * (1.0 - transparentColor.a);
alpha = 1.0;
}
} else {
// blending
color = transparentColor + color * (1.0 - transparentColor.a);
alpha = transparentColor.a + alpha * (1.0 - transparentColor.a);
}
}
}
#endif

View File

@@ -121,7 +121,7 @@ void main() {
gl_FragColor = vec4(packIntToRGB(float(uObjectId)), 1.0);
gl_FragData[1] = vec4(packIntToRGB(vInstance), 1.0);
gl_FragData[2] = vec4(texture2D(tGroupTex, vUv).rgb, 1.0);
gl_FragData[3] = packDepthToRGBA(gl_FragCoord.z);
gl_FragData[3] = packDepthToRGBA(fragmentDepth);
#else
gl_FragColor = vColor;
if (uPickType == 1) {
@@ -135,7 +135,11 @@ void main() {
#elif defined(dRenderVariant_depth)
if (imageData.a < 0.05)
discard;
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
if (uRenderMask == MaskOpaque) {
gl_FragColor = packDepthToRGBA(fragmentDepth);
} else if (uRenderMask == MaskTransparent) {
gl_FragColor = packDepthWithAlphaToRGBA(fragmentDepth, imageData.a);
}
#elif defined(dRenderVariant_marking)
float marker = uMarker;
if (uMarker == -1.0) {
@@ -146,7 +150,7 @@ void main() {
if (uMarkingType == 1) {
if (marker > 0.0 || imageData.a < 0.05)
discard;
gl_FragColor = packDepthToRGBA(gl_FragCoord.z);
gl_FragColor = packDepthToRGBA(fragmentDepth);
} else {
if (marker == 0.0 || imageData.a < 0.05)
discard;

View File

@@ -38,9 +38,14 @@ vec4 texture3dFrom2dNearest(sampler2D tex, vec3 pos, vec3 gridDim, vec2 texDim)
return texture2D(tex, coord);
}
vec4 voxel(vec3 pos) {
float voxelValue(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(1.0));
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
void main(void) {
@@ -48,14 +53,14 @@ void main(void) {
vec3 posXYZ = index3dFrom2d(uv);
// get MC case as the sum of corners that are below the given iso level
float c = step(voxel(posXYZ).a, uIsoValue)
+ 2. * step(voxel(posXYZ + c1).a, uIsoValue)
+ 4. * step(voxel(posXYZ + c2).a, uIsoValue)
+ 8. * step(voxel(posXYZ + c3).a, uIsoValue)
+ 16. * step(voxel(posXYZ + c4).a, uIsoValue)
+ 32. * step(voxel(posXYZ + c5).a, uIsoValue)
+ 64. * step(voxel(posXYZ + c6).a, uIsoValue)
+ 128. * step(voxel(posXYZ + c7).a, uIsoValue);
float c = step(voxelValue(posXYZ), uIsoValue)
+ 2. * step(voxelValue(posXYZ + c1), uIsoValue)
+ 4. * step(voxelValue(posXYZ + c2), uIsoValue)
+ 8. * step(voxelValue(posXYZ + c3), uIsoValue)
+ 16. * step(voxelValue(posXYZ + c4), uIsoValue)
+ 32. * step(voxelValue(posXYZ + c5), uIsoValue)
+ 64. * step(voxelValue(posXYZ + c6), uIsoValue)
+ 128. * step(voxelValue(posXYZ + c7), uIsoValue);
c *= step(c, 254.);
// handle out of bounds positions

View File

@@ -59,9 +59,14 @@ vec4 voxel(vec3 pos) {
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
}
vec4 voxelPadded(vec3 pos) {
float voxelValuePadded(vec3 pos) {
pos = min(max(vec3(0.0), pos), uGridDim - vec3(vec2(2.0), 1.0)); // remove xy padding
return texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
vec4 v = texture3dFrom2dNearest(tVolumeData, pos / uGridDim, uGridDim, uGridTexDim.xy);
#ifdef dValueChannel_red
return v.r;
#else
return v.a;
#endif
}
int idot2(const in ivec2 a, const in ivec2 b) {
@@ -261,8 +266,13 @@ void main(void) {
vec4 d0 = voxel(b0);
vec4 d1 = voxel(b1);
float v0 = d0.a;
float v1 = d1.a;
#ifdef dValueChannel_red
float v0 = d0.r;
float v1 = d1.r;
#else
float v0 = d0.a;
float v1 = d1.a;
#endif
float t = (uIsoValue - v0) / (v0 - v1);
gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
@@ -286,14 +296,14 @@ void main(void) {
// normals from gradients
vec3 n0 = -normalize(vec3(
voxelPadded(b0 - c1).a - voxelPadded(b0 + c1).a,
voxelPadded(b0 - c3).a - voxelPadded(b0 + c3).a,
voxelPadded(b0 - c4).a - voxelPadded(b0 + c4).a
voxelValuePadded(b0 - c1) - voxelValuePadded(b0 + c1),
voxelValuePadded(b0 - c3) - voxelValuePadded(b0 + c3),
voxelValuePadded(b0 - c4) - voxelValuePadded(b0 + c4)
));
vec3 n1 = -normalize(vec3(
voxelPadded(b1 - c1).a - voxelPadded(b1 + c1).a,
voxelPadded(b1 - c3).a - voxelPadded(b1 + c3).a,
voxelPadded(b1 - c4).a - voxelPadded(b1 + c4).a
voxelValuePadded(b1 - c1) - voxelValuePadded(b1 + c1),
voxelValuePadded(b1 - c3) - voxelValuePadded(b1 + c3),
voxelValuePadded(b1 - c4) - voxelValuePadded(b1 + c4)
));
gl_FragData[2].xyz = -vec3(
n0.x + t * (n0.x - n1.x),
@@ -307,6 +317,6 @@ void main(void) {
}
// apply normal matrix
gl_FragData[2].xyz *= transpose3(inverse3(mat3(uGridTransform)));
gl_FragData[2].xyz *= adjoint(uGridTransform);
}
`;

View File

@@ -34,6 +34,16 @@ void main() {
#endif
float fragmentDepth = gl_FragCoord.z;
#ifdef dNeedsNormal
#if defined(dFlatShaded)
vec3 normal = -faceNormal;
#else
vec3 normal = -normalize(vNormal);
if (uDoubleSided) normal *= float(frontFacing) * 2.0 - 1.0;
#endif
#endif
#include assign_material_color
#include check_transparency
@@ -54,12 +64,6 @@ void main() {
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
#if defined(dFlatShaded)
vec3 normal = -faceNormal;
#else
vec3 normal = -normalize(vNormal);
if (uDoubleSided) normal *= float(frontFacing) * 2.0 - 1.0;
#endif
#include apply_light_color
#include apply_interior_color
#include apply_marker_color

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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -44,7 +44,7 @@ void main(){
#else
vec3 normal = aNormal;
#endif
mat3 normalMatrix = transpose3(inverse3(mat3(modelView)));
mat3 normalMatrix = adjoint(modelView);
vec3 transformedNormal = normalize(normalMatrix * normalize(normal));
#if defined(dFlipSided)
if (!uDoubleSided) { // TODO checking uDoubleSided should not be required, ASR

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 Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -40,7 +40,7 @@ float getDepthOpaque(const in vec2 coords) {
float getDepthTransparent(const in vec2 coords) {
#ifdef dTransparentOutline
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
#else
return 1.0;
#endif

View File

@@ -1,8 +1,9 @@
/**
* 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 Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
* @author Gianluca Tomasello <giagitom@gmail.com>
*/
export const postprocessing_frag = `
@@ -11,8 +12,11 @@ precision highp int;
precision highp sampler2D;
uniform sampler2D tSsaoDepth;
uniform sampler2D tSsaoDepthTransparent;
uniform sampler2D tColor;
uniform sampler2D tTransparentColor;
uniform sampler2D tDepthOpaque;
uniform sampler2D tDepthTransparent;
uniform sampler2D tShadows;
uniform sampler2D tOutlines;
uniform vec2 uTexSize;
@@ -45,19 +49,24 @@ float getDepthOpaque(const in vec2 coords) {
#endif
}
bool isBackground(const in float depth) {
return depth == 1.0;
float getDepthTransparent(const in vec2 coords) {
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, coords)).x;
}
float getOutline(const in vec2 coords, const in float opaqueDepth, out float closestTexel) {
float backgroundViewZ = 2.0 * uFar;
bool isBackground(const in float depth) {
return depth > 0.999; // handle depth packing precision issues
}
int squaredOutlineScale = dOutlineScale * dOutlineScale;
float getOutline(const in vec2 coords, const in float opaqueDepth, const in float transparentDepth, out float closestTexel, out float isTransparent) {
vec2 invTexSize = 1.0 / uTexSize;
float outline = 1.0;
closestTexel = 1.0;
isTransparent = 0.0;
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
for (int x = -dOutlineScale; x <= dOutlineScale; x++) {
if (x * x + y * y > dOutlineScale * dOutlineScale) {
if (x * x + y * y > squaredOutlineScale) {
continue;
}
@@ -70,10 +79,11 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
outline = 0.0;
closestTexel = sampleOutlineDepth;
isTransparent = sampleOutlineCombined.a;
}
}
}
return closestTexel < opaqueDepth ? outline : 1.0;
return isTransparent == 0.0 ? outline : (closestTexel > opaqueDepth && closestTexel < transparentDepth) ? 1.0 : outline;
}
float getSsao(vec2 coords) {
@@ -87,31 +97,60 @@ float getSsao(vec2 coords) {
return 1.0;
}
float getSsaoTransparent(vec2 coords) {
float rawSsao = unpackRGToUnitInterval(texture2D(tSsaoDepthTransparent, coords).xy);
if (rawSsao > 0.999) {
return 1.0;
} else if (rawSsao > 0.001) {
return rawSsao;
}
// treat values close to 0.0 as errors and return no occlusion
return 1.0;
}
void main(void) {
vec2 coords = gl_FragCoord.xy / uTexSize;
vec4 color = texture2D(tColor, coords);
float viewDist;
float fogFactor;
float opaqueDepth = getDepthOpaque(coords);
float transparentDepth = 1.0;
#ifdef dBlendTransparency
bool blendTransparency = true;
vec4 transparentColor = texture2D(tTransparentColor, coords);
#ifdef dOcclusionEnable
if (!isBackground(opaqueDepth)) {
viewDist = abs(getViewZ(opaqueDepth));
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
#if defined(dOutlineEnable) || defined(dOcclusionEnable) && defined(dOcclusionIncludeTransparency)
transparentDepth = getDepthTransparent(coords);
#endif
#endif
#if defined(dOcclusionEnable) || defined(dShadowEnable)
bool isOpaqueBackground = isBackground(opaqueDepth);
float viewDist = abs(getViewZ(opaqueDepth));
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
#endif
#if defined(dOcclusionEnable)
if (!isOpaqueBackground) {
float occlusionFactor = getSsao(coords + uOcclusionOffset);
if (!uTransparentBackground) {
color.rgb = mix(mix(uOcclusionColor, uFogColor, fogFactor), color.rgb, occlusionFactor);
} else {
color.rgb = mix(uOcclusionColor * (1.0 - fogFactor), color.rgb, occlusionFactor);
}
}
#if defined(dBlendTransparency) && defined(dOcclusionIncludeTransparency)
if (!isBackground(transparentDepth)) {
float viewDist = abs(getViewZ(transparentDepth));
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
float occlusionFactor = getSsaoTransparent(coords + uOcclusionOffset);
transparentColor.rgb = mix(uOcclusionColor * (1.0 - fogFactor), transparentColor.rgb, occlusionFactor);
}
#endif
#endif
#ifdef dShadowEnable
if (!isBackground(opaqueDepth)) {
viewDist = abs(getViewZ(opaqueDepth));
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
if (!isOpaqueBackground) {
vec4 shadow = texture2D(tShadows, coords);
if (!uTransparentBackground) {
color.rgb = mix(mix(vec3(0), uFogColor, fogFactor), color.rgb, shadow.a);
@@ -124,15 +163,31 @@ void main(void) {
// outline needs to be handled after occlusion and shadow to keep them clean
#ifdef dOutlineEnable
float closestTexel;
float outline = getOutline(coords, opaqueDepth, closestTexel);
float isTransparentOutline;
float outline = getOutline(coords, opaqueDepth, transparentDepth, closestTexel, isTransparentOutline);
if (outline == 0.0) {
viewDist = abs(getViewZ(closestTexel));
fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
float viewDist = abs(getViewZ(closestTexel));
float fogFactor = smoothstep(uFogNear, uFogFar, viewDist);
if (!uTransparentBackground) {
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
color.rgb = mix(uOutlineColor, uFogColor, fogFactor);
} else {
color.a = 1.0 - fogFactor;
color.rgb = mix(uOutlineColor, color.rgb, fogFactor);
color.rgb = mix(uOutlineColor, vec3(0.0), fogFactor);
}
#ifdef dBlendTransparency
if (isTransparentOutline == 1.0 || transparentDepth > closestTexel) {
blendTransparency = false;
}
#endif
}
#endif
#ifdef dBlendTransparency
if (blendTransparency) {
float alpha = transparentColor.a;
if (alpha != 0.0) {
// blending
color = transparentColor + color * (1.0 - alpha);
}
}
#endif

View File

@@ -115,6 +115,11 @@ void main(void){
#if !defined(dClipPrimitive) && defined(dClipVariant_pixel) && dClipObjectCount != 0
#include clip_pixel
#endif
#ifdef dNeedsNormal
vec3 normal = -cameraNormal;
#endif
#include assign_material_color
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
@@ -142,7 +147,6 @@ void main(void){
#elif defined(dRenderVariant_emissive)
gl_FragColor = material;
#elif defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
vec3 normal = -cameraNormal;
#include apply_light_color
#include apply_interior_color
#include apply_marker_color

View File

@@ -36,7 +36,8 @@ float getViewZ(const in float depth) {
}
bool isBackground(const in float depth) {
return depth == 1.0;
// checking for 1.0 is not enough, because of precision issues
return depth >= 0.999;
}
bool isNearClip(const in float depth) {

View File

@@ -4,6 +4,7 @@
* @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>
*/
export const ssao_frag = `
@@ -13,9 +14,19 @@ precision highp sampler2D;
#include common
uniform sampler2D tDepth;
uniform sampler2D tDepthHalf;
uniform sampler2D tDepthQuarter;
#if defined(dIncludeTransparent)
uniform sampler2D tDepthTransparent;
uniform sampler2D tDepthHalfTransparent;
uniform sampler2D tDepthQuarterTransparent;
#endif
uniform int uTransparencyFlag;
uniform vec2 uTexSize;
uniform vec4 uBounds;
@@ -53,18 +64,33 @@ vec2 getNoiseVec2(const in vec2 coords) {
}
bool isBackground(const in float depth) {
return depth == 1.0;
return depth > 0.999; // handle precision issues with packed depth
}
float getDepth(const in vec2 coords) {
float getDepth(const in vec2 coords, const in int transparentFlag) {
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
#ifdef depthTextureSupport
return texture2D(tDepth, c).r;
#else
return unpackRGBAToDepth(texture2D(tDepth, c));
#endif
if (transparentFlag == 1){
#if defined(dIncludeTransparent)
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c)).x;
#else
return 1.0;
#endif
} else {
#ifdef depthTextureSupport
return texture2D(tDepth, c).r;
#else
return unpackRGBAToDepth(texture2D(tDepth, c));
#endif
}
}
#if defined(dIncludeTransparent)
vec2 getDepthTransparentWithAlpha(const in vec2 coords){
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c));
}
#endif
#define dQuarterThreshold 0.1
#define dHalfThreshold 0.05
@@ -90,19 +116,33 @@ float getMappedDepth(const in vec2 coords, const in vec2 selfCoords) {
#endif
}
#if defined(dIncludeTransparent)
vec2 getMappedDepthTransparentWithAlpha(const in vec2 coords, const in vec2 selfCoords) {
vec2 c = vec2(clamp(coords.x, uBounds.x, uBounds.z), clamp(coords.y, uBounds.y, uBounds.w));
float d = distance(coords, selfCoords);
if (d > dQuarterThreshold) {
return unpackRGBAToDepthWithAlpha(texture2D(tDepthQuarterTransparent, c));
} else if (d > dHalfThreshold) {
return unpackRGBAToDepthWithAlpha(texture2D(tDepthHalfTransparent, c));
} else {
return unpackRGBAToDepthWithAlpha(texture2D(tDepthTransparent, c));
}
}
#endif
// adapted from https://gist.github.com/bgolus/a07ed65602c009d5e2f753826e8078a0
vec3 viewNormalAtPixelPositionAccurate(vec2 vpos) {
vec3 viewNormalAtPixelPositionAccurate(const in vec2 vpos, const in int transparentFlag) {
// current pixel's depth
float c = getDepth(vpos);
float c = getDepth(vpos, transparentFlag);
// get current pixel's view space position
vec3 viewSpacePos_c = screenSpaceToViewSpace(vec3(vpos, c), uInvProjection);
// get view space position at 1 pixel offsets in each major direction
vec3 viewSpacePos_l = screenSpaceToViewSpace(vec3(vpos + vec2(-1.0, 0.0) / uTexSize, getDepth(vpos + vec2(-1.0, 0.0) / uTexSize)), uInvProjection);
vec3 viewSpacePos_r = screenSpaceToViewSpace(vec3(vpos + vec2( 1.0, 0.0) / uTexSize, getDepth(vpos + vec2( 1.0, 0.0) / uTexSize)), uInvProjection);
vec3 viewSpacePos_d = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0,-1.0) / uTexSize, getDepth(vpos + vec2( 0.0,-1.0) / uTexSize)), uInvProjection);
vec3 viewSpacePos_u = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0, 1.0) / uTexSize, getDepth(vpos + vec2( 0.0, 1.0) / uTexSize)), uInvProjection);
vec3 viewSpacePos_l = screenSpaceToViewSpace(vec3(vpos + vec2(-1.0, 0.0) / uTexSize, getDepth(vpos + vec2(-1.0, 0.0) / uTexSize, transparentFlag)), uInvProjection);
vec3 viewSpacePos_r = screenSpaceToViewSpace(vec3(vpos + vec2( 1.0, 0.0) / uTexSize, getDepth(vpos + vec2( 1.0, 0.0) / uTexSize, transparentFlag)), uInvProjection);
vec3 viewSpacePos_d = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0,-1.0) / uTexSize, getDepth(vpos + vec2( 0.0,-1.0) / uTexSize, transparentFlag)), uInvProjection);
vec3 viewSpacePos_u = screenSpaceToViewSpace(vec3(vpos + vec2( 0.0, 1.0) / uTexSize, getDepth(vpos + vec2( 0.0, 1.0) / uTexSize, transparentFlag)), uInvProjection);
// get the difference between the current and each offset position
vec3 l = viewSpacePos_c - viewSpacePos_l;
@@ -112,18 +152,18 @@ vec3 viewNormalAtPixelPositionAccurate(vec2 vpos) {
// get depth values at 1 & 2 pixels offsets from current along the horizontal axis
vec4 H = vec4(
getDepth(vpos + vec2(-1.0, 0.0) / uTexSize),
getDepth(vpos + vec2( 1.0, 0.0) / uTexSize),
getDepth(vpos + vec2(-2.0, 0.0) / uTexSize),
getDepth(vpos + vec2( 2.0, 0.0) / uTexSize)
getDepth(vpos + vec2(-1.0, 0.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2( 1.0, 0.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2(-2.0, 0.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2( 2.0, 0.0) / uTexSize, transparentFlag)
);
// get depth values at 1 & 2 pixels offsets from current along the vertical axis
vec4 V = vec4(
getDepth(vpos + vec2(0.0,-1.0) / uTexSize),
getDepth(vpos + vec2(0.0, 1.0) / uTexSize),
getDepth(vpos + vec2(0.0,-2.0) / uTexSize),
getDepth(vpos + vec2(0.0, 2.0) / uTexSize)
getDepth(vpos + vec2(0.0,-1.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2(0.0, 1.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2(0.0,-2.0) / uTexSize, transparentFlag),
getDepth(vpos + vec2(0.0, 2.0) / uTexSize, transparentFlag)
);
// current pixel's depth difference from slope of offset depth samples
@@ -152,8 +192,7 @@ float getPixelSize(const in vec2 coords, const in float depth) {
void main(void) {
vec2 invTexSize = 1.0 / uTexSize;
vec2 selfCoords = gl_FragCoord.xy * invTexSize;
float selfDepth = getDepth(selfCoords);
float selfDepth = getDepth(selfCoords, uTransparencyFlag);
vec2 selfPackedDepth = packUnitIntervalToRG(selfDepth);
if (isBackground(selfDepth)) {
@@ -161,7 +200,7 @@ void main(void) {
return;
}
vec3 selfViewNormal = viewNormalAtPixelPositionAccurate(selfCoords);
vec3 selfViewNormal = viewNormalAtPixelPositionAccurate(selfCoords, uTransparencyFlag);
vec3 selfViewPos = screenSpaceToViewSpace(vec3(selfCoords, selfDepth), uInvProjection);
vec3 randomVec = normalize(vec3(getNoiseVec2(selfCoords) * 2.0 - 1.0, 0.0));
@@ -190,9 +229,26 @@ void main(void) {
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
// get sample depth:
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
levelOcclusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
float sampleOcc = 0.0;
#ifdef dIllumination
if (uTransparencyFlag == 1) {
#endif
float sampleDepth = getMappedDepth(offset.xy, selfCoords);
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
sampleOcc = step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l];
#ifdef dIllumination
}
#endif
#if defined(dIncludeTransparent)
vec2 sampleDepthWithAlpha = getMappedDepthTransparentWithAlpha(offset.xy, selfCoords);
if (!isBackground(sampleDepthWithAlpha.x)) {
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepthWithAlpha.x), uInvProjection).z;
sampleOcc = max(sampleOcc, step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uLevelRadius[l] / abs(selfViewPos.z - sampleViewZ)) * uLevelBias[l] * sampleDepthWithAlpha.y);
}
#endif
levelOcclusion += sampleOcc;
}
occlusion = max(occlusion, levelOcclusion);
}
@@ -205,11 +261,27 @@ void main(void) {
offset = uProjection * offset;
offset.xyz = (offset.xyz / offset.w) * 0.5 + 0.5;
// NOTE: using getMappedDepth here causes issues on some mobile devices
float sampleDepth = getDepth(offset.xy);
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
float sampleOcc = 0.0;
#ifdef dIllumination
if (uTransparencyFlag == 1) {
#endif
// NOTE: using getMappedDepth here causes issues on some mobile devices
float sampleDepth = getDepth(offset.xy, 0);
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepth), uInvProjection).z;
occlusion += step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
sampleOcc = step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ));
#ifdef dIllumination
}
#endif
#if defined(dIncludeTransparent)
vec2 sampleDepthWithAlpha = getDepthTransparentWithAlpha(offset.xy);
if (!isBackground(sampleDepthWithAlpha.x)) {
float sampleViewZ = screenSpaceToViewSpace(vec3(offset.xy, sampleDepthWithAlpha.x), uInvProjection).z;
sampleOcc = max(sampleOcc, step(sampleViewPos.z + 0.025, sampleViewZ) * smootherstep(0.0, 1.0, uRadius / abs(selfViewPos.z - sampleViewZ)) * sampleDepthWithAlpha.y);
}
#endif
occlusion += sampleOcc;
}
#endif
occlusion = 1.0 - (uBias * occlusion / float(dNSamples));

View File

@@ -22,13 +22,6 @@ uniform float uBackgroundOpacity;
varying vec2 vTexCoord;
const float smoothness = 32.0;
const float gamma = 2.2;
void main2(){
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
void main(){
#include fade_lod
#include clip_pixel
@@ -44,20 +37,13 @@ void main(){
// retrieve signed distance
float sdf = texture2D(tFont, vTexCoord).a + uBorderWidth;
// perform adaptive anti-aliasing of the edges
float w = clamp(smoothness * (abs(dFdx(vTexCoord.x)) + abs(dFdy(vTexCoord.y))), 0.0, 0.5);
float a = clamp(0.0, 1.0, smoothstep(0.5 - w, 0.5 + w, sdf));
// gamma correction for linear attenuation
a = pow(a, 1.0 / gamma);
if (a < 0.5) discard;
if (sdf < 0.5) discard;
#if defined(dRenderVariant_color) || defined(dRenderVariant_tracing)
// add border
float t = 0.5 + uBorderWidth;
if (uBorderWidth > 0.0 && sdf < t) {
material.xyz = mix(uBorderColor, material.xyz, smoothstep(t - w, t, sdf));
material.xyz = uBorderColor;
}
#endif
}

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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -792,6 +792,86 @@ export function getClipControl(gl: GLRenderingContext): COMPAT_clip_control | nu
return null;
}
/**
* See https://registry.khronos.org/webgl/extensions/EXT_render_snorm/
*/
export interface COMPAT_render_snorm {
}
export function getRenderSnorm(gl: GLRenderingContext): COMPAT_render_snorm | null {
if (isWebGL2(gl)) {
const ext = gl.getExtension('EXT_render_snorm');
if (ext) {
return {};
}
}
return null;
}
/**
* See https://registry.khronos.org/webgl/extensions/WEBGL_render_shared_exponent/
*/
export interface COMPAT_render_shared_exponent {
}
export function getRenderSharedExponent(gl: GLRenderingContext): COMPAT_render_shared_exponent | null {
if (isWebGL2(gl)) {
const ext = gl.getExtension('WEBGL_render_shared_exponent');
if (ext) {
return {};
}
}
return null;
}
/**
* See https://registry.khronos.org/webgl/extensions/EXT_texture_norm16/
*/
export interface COMPAT_texture_norm16 {
readonly R16: number;
readonly RG16: number;
readonly RGB16: number;
readonly RGBA16: number;
readonly R16_SNORM: number;
readonly RG16_SNORM: number;
readonly RGB16_SNORM: number;
readonly RGBA16_SNORM: number;
}
export function getTextureNorm16(gl: GLRenderingContext): COMPAT_texture_norm16 | null {
const ext = gl.getExtension('EXT_texture_norm16');
if (ext) {
return {
R16: ext.R16_EXT,
RG16: ext.RG16_EXT,
RGB16: ext.RGB16_EXT,
RGBA16: ext.RGBA16_EXT,
R16_SNORM: ext.R16_SNORM_EXT,
RG16_SNORM: ext.RG16_SNORM_EXT,
RGB16_SNORM: ext.RGB16_SNORM_EXT,
RGBA16_SNORM: ext.RGBA16_SNORM_EXT
};
}
return null;
}
/**
* See https://registry.khronos.org/webgl/extensions/EXT_depth_clamp/
*/
export interface COMPAT_depth_clamp {
readonly DEPTH_CLAMP: number;
}
export function getDepthClamp(gl: GLRenderingContext): COMPAT_depth_clamp | null {
const ext = gl.getExtension('EXT_depth_clamp');
if (ext) {
return {
DEPTH_CLAMP: ext.DEPTH_CLAMP_EXT
};
}
return null;
}
export function getNoNonInstancedActiveAttribs(gl: GLRenderingContext): boolean {
if (!isWebGL2(gl)) return false;

View File

@@ -1,10 +1,10 @@
/**
* 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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query, getNoNonInstancedActiveAttribs, COMPAT_multi_draw, getMultiDraw, getDrawInstancedBaseVertexBaseInstance, getMultiDrawInstancedBaseVertexBaseInstance, COMPAT_draw_instanced_base_vertex_base_instance, COMPAT_multi_draw_instanced_base_vertex_base_instance, getDrawBuffersIndexed, COMPAT_draw_buffers_indexed, getParallelShaderCompile, COMPAT_parallel_shader_compile, getFboRenderMipmap, COMPAT_fboRenderMipmap, COMPAT_provoking_vertex, getProvokingVertex, COMPAT_clip_cull_distance, getClipCullDistance, COMPAT_conservative_depth, getConservativeDepth, COMPAT_stencil_texturing, getStencilTexturing, COMPAT_clip_control, getClipControl } from './compat';
import { GLRenderingContext, COMPAT_instanced_arrays, COMPAT_standard_derivatives, COMPAT_vertex_array_object, getInstancedArrays, getStandardDerivatives, COMPAT_element_index_uint, getElementIndexUint, COMPAT_texture_float, getTextureFloat, COMPAT_texture_float_linear, getTextureFloatLinear, COMPAT_blend_minmax, getBlendMinMax, getFragDepth, COMPAT_frag_depth, COMPAT_color_buffer_float, getColorBufferFloat, COMPAT_draw_buffers, getDrawBuffers, getShaderTextureLod, COMPAT_shader_texture_lod, getDepthTexture, COMPAT_depth_texture, COMPAT_sRGB, getSRGB, getTextureHalfFloat, getTextureHalfFloatLinear, COMPAT_texture_half_float, COMPAT_texture_half_float_linear, COMPAT_color_buffer_half_float, getColorBufferHalfFloat, getVertexArrayObject, getDisjointTimerQuery, COMPAT_disjoint_timer_query, getNoNonInstancedActiveAttribs, COMPAT_multi_draw, getMultiDraw, getDrawInstancedBaseVertexBaseInstance, getMultiDrawInstancedBaseVertexBaseInstance, COMPAT_draw_instanced_base_vertex_base_instance, COMPAT_multi_draw_instanced_base_vertex_base_instance, getDrawBuffersIndexed, COMPAT_draw_buffers_indexed, getParallelShaderCompile, COMPAT_parallel_shader_compile, getFboRenderMipmap, COMPAT_fboRenderMipmap, COMPAT_provoking_vertex, getProvokingVertex, COMPAT_clip_cull_distance, getClipCullDistance, COMPAT_conservative_depth, getConservativeDepth, COMPAT_stencil_texturing, getStencilTexturing, COMPAT_clip_control, getClipControl, getRenderSnorm, COMPAT_render_snorm, getRenderSharedExponent, COMPAT_render_shared_exponent, getTextureNorm16, COMPAT_texture_norm16, getDepthClamp, COMPAT_depth_clamp } from './compat';
import { isDebugMode } from '../../mol-util/debug';
export type WebGLExtensions = {
@@ -37,6 +37,10 @@ export type WebGLExtensions = {
conservativeDepth: COMPAT_conservative_depth | null
stencilTexturing: COMPAT_stencil_texturing | null
clipControl: COMPAT_clip_control | null
renderSnorm: COMPAT_render_snorm | null
renderSharedExponent: COMPAT_render_shared_exponent | null
textureNorm16: COMPAT_texture_norm16 | null
depthClamp: COMPAT_depth_clamp | null
noNonInstancedActiveAttribs: boolean
}
@@ -161,6 +165,22 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
if (isDebugMode && clipControl === null) {
console.log('Could not find support for "clip_control"');
}
const renderSnorm = getRenderSnorm(gl);
if (isDebugMode && renderSnorm === null) {
console.log('Could not find support for "render_snorm"');
}
const renderSharedExponent = getRenderSharedExponent(gl);
if (isDebugMode && renderSharedExponent === null) {
console.log('Could not find support for "render_shared_exponent"');
}
const textureNorm16 = getTextureNorm16(gl);
if (isDebugMode && textureNorm16 === null) {
console.log('Could not find support for "texture_norm16"');
}
const depthClamp = getDepthClamp(gl);
if (isDebugMode && depthClamp === null) {
console.log('Could not find support for "depth_clamp"');
}
const noNonInstancedActiveAttribs = getNoNonInstancedActiveAttribs(gl);
@@ -194,6 +214,10 @@ export function createExtensions(gl: GLRenderingContext): WebGLExtensions {
conservativeDepth,
stencilTexturing,
clipControl,
renderSnorm,
renderSharedExponent,
textureNorm16,
depthClamp,
noNonInstancedActiveAttribs,
};

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 Alexander Rose <alexander.rose@weirdbyte.de>
*/
@@ -24,6 +24,7 @@ export type WebGLState = {
* - `gl.SCISSOR_TEST`: scissor test that discards fragments that are outside of the scissor rectangle
* - `gl.STENCIL_TEST`: stencil testing and updates to the stencil buffer
* - `ext.CLIP_DISTANCE[0-7]`: clip distance 0 to 7 (with `ext` being `WEBGL_clip_cull_distance`)
* - `ext.DEPTH_CLAMP`: depth clamping (with `ext` being `EXT_depth_clamp`)
*/
enable: (cap: number) => void
/**
@@ -38,6 +39,7 @@ export type WebGLState = {
* - `gl.SCISSOR_TEST`: scissor test that discards fragments that are outside of the scissor rectangle
* - `gl.STENCIL_TEST`: stencil testing and updates to the stencil buffer
* - `ext.CLIP_DISTANCE[0-7]`: clip distance 0 to 7 (with `ext` being `WEBGL_clip_cull_distance`)
* - `ext.DEPTH_CLAMP`: depth clamping (with `ext` being `EXT_depth_clamp`)
*/
disable: (cap: number) => void

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