mirror of
https://github.com/molstar/molstar.git
synced 2026-06-04 21:34:23 +08:00
Compare commits
425 Commits
v4.4.0
...
support-sc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bd162b977 | ||
|
|
c7fb71738e | ||
|
|
9413481253 | ||
|
|
9f3c617945 | ||
|
|
f920188cdc | ||
|
|
68b73503bb | ||
|
|
e776138ecd | ||
|
|
bbacd5a9dd | ||
|
|
289ecef1d7 | ||
|
|
52fc3ef750 | ||
|
|
dffe40ac1d | ||
|
|
f834e39ce4 | ||
|
|
2bc9c6fb57 | ||
|
|
6e42c11f5e | ||
|
|
d48feeaa94 | ||
|
|
fd0ca75fc1 | ||
|
|
a270dcb5f5 | ||
|
|
917de1175c | ||
|
|
65945fb904 | ||
|
|
3b7afc6037 | ||
|
|
05d9ca6e68 | ||
|
|
12ee0e0f38 | ||
|
|
dbd29e749e | ||
|
|
a8085111dc | ||
|
|
93798554ac | ||
|
|
ce07c52d9f | ||
|
|
fb7a247f6c | ||
|
|
e9dfe6322d | ||
|
|
079187326a | ||
|
|
4dc9d037a4 | ||
|
|
f36ad9ac28 | ||
|
|
6d392de628 | ||
|
|
d7cd957b42 | ||
|
|
de36612bf1 | ||
|
|
d5154bcff2 | ||
|
|
b44a6fa660 | ||
|
|
5cc28c9471 | ||
|
|
b42a6d4636 | ||
|
|
efd405f44b | ||
|
|
4b3932e9e2 | ||
|
|
dcb8eca29a | ||
|
|
ac0177aef5 | ||
|
|
316013aafd | ||
|
|
040d83e8d4 | ||
|
|
b31ed50b3a | ||
|
|
2a9c4db97f | ||
|
|
fbeda779ac | ||
|
|
89e60cfde9 | ||
|
|
0845f5fd75 | ||
|
|
918b67482f | ||
|
|
3ff3ea2912 | ||
|
|
b2e1d069ba | ||
|
|
0a409c6fdf | ||
|
|
5ce552d2cc | ||
|
|
8bda510378 | ||
|
|
ad1923f57b | ||
|
|
ba38fe2474 | ||
|
|
c53b651472 | ||
|
|
2eb4f77504 | ||
|
|
c09f30a135 | ||
|
|
c60c52f563 | ||
|
|
7e67678dcd | ||
|
|
4ee33c9dcd | ||
|
|
8a0d5eb366 | ||
|
|
e18a3b452a | ||
|
|
38a508fd87 | ||
|
|
0b1fd14e09 | ||
|
|
b883ddd10e | ||
|
|
30557d13ca | ||
|
|
85b72ae3b0 | ||
|
|
2ed165f9a5 | ||
|
|
8c5388a6ea | ||
|
|
703ef6c273 | ||
|
|
0a1c5537d2 | ||
|
|
e65f5b270e | ||
|
|
9185c4592f | ||
|
|
fbe44bfab7 | ||
|
|
f4d44621d6 | ||
|
|
05a87fded9 | ||
|
|
195f7284b5 | ||
|
|
c4a900e2ea | ||
|
|
e1eb686355 | ||
|
|
54b4a01cc3 | ||
|
|
f68a01183d | ||
|
|
057d605135 | ||
|
|
a391bbf786 | ||
|
|
fdc1054060 | ||
|
|
b4238f574a | ||
|
|
965c6a37a9 | ||
|
|
35a9056368 | ||
|
|
fd96973e82 | ||
|
|
8812b0d264 | ||
|
|
597c0dbbe1 | ||
|
|
768d7a2a4d | ||
|
|
30ec53ffa4 | ||
|
|
b79ffd9cfc | ||
|
|
cc7f88fd53 | ||
|
|
57c84d0159 | ||
|
|
4daf409337 | ||
|
|
a17e886ab9 | ||
|
|
ebb9046184 | ||
|
|
cb41c0c7f9 | ||
|
|
bdbc9eab64 | ||
|
|
5f76620ef5 | ||
|
|
b5c1c4d32e | ||
|
|
6e4777355a | ||
|
|
7526535a8b | ||
|
|
2bd84b7e7c | ||
|
|
84e292b3e2 | ||
|
|
152cef9c5b | ||
|
|
d76bc583c0 | ||
|
|
071fb21dd0 | ||
|
|
db8943bcfb | ||
|
|
2d1ce14f2e | ||
|
|
33760b0d37 | ||
|
|
1aa6f30780 | ||
|
|
86c8dd5d74 | ||
|
|
1435a5e6e6 | ||
|
|
c123e55a8d | ||
|
|
c37a7ebf79 | ||
|
|
00e228a834 | ||
|
|
55f40738f2 | ||
|
|
4ffd69750f | ||
|
|
295608baae | ||
|
|
4429b7185f | ||
|
|
84fadc2e5c | ||
|
|
0b3bd885ca | ||
|
|
51d9eda168 | ||
|
|
abe10d5c7c | ||
|
|
e7da2333fe | ||
|
|
3899a95c97 | ||
|
|
12add4d66b | ||
|
|
e16c073639 | ||
|
|
3c5dc56bb2 | ||
|
|
ad2106e6f6 | ||
|
|
dd5aa061b8 | ||
|
|
f69ad14296 | ||
|
|
277254b78e | ||
|
|
3c4f2806e7 | ||
|
|
79612833d4 | ||
|
|
b4772e0cb9 | ||
|
|
003c5a9437 | ||
|
|
ff9fb450fa | ||
|
|
136e996e4f | ||
|
|
a93b53c413 | ||
|
|
0f25421db1 | ||
|
|
cde3a73bba | ||
|
|
c19130c9eb | ||
|
|
54c8801951 | ||
|
|
8371a3e349 | ||
|
|
cca289728c | ||
|
|
d9a44daa5d | ||
|
|
48ee9ef8cb | ||
|
|
ba84081888 | ||
|
|
45d8059ed2 | ||
|
|
6e2d8653ec | ||
|
|
cca6210076 | ||
|
|
9f926757b2 | ||
|
|
87d83d8f9e | ||
|
|
d16076b170 | ||
|
|
cccdc53fd0 | ||
|
|
a312799361 | ||
|
|
60c81e79ba | ||
|
|
bd22db4252 | ||
|
|
36b5a9e181 | ||
|
|
809cca5261 | ||
|
|
7a81ea3ba1 | ||
|
|
afa51b4416 | ||
|
|
95792dd3c8 | ||
|
|
e2bc15ac6b | ||
|
|
4e565808c6 | ||
|
|
b2e2b46280 | ||
|
|
462e675237 | ||
|
|
6e77b4ce71 | ||
|
|
e8bd67c069 | ||
|
|
fe502539f9 | ||
|
|
fe5afa8935 | ||
|
|
20452e762b | ||
|
|
bc2d19338b | ||
|
|
719e141dd9 | ||
|
|
5d9d01d251 | ||
|
|
39ad2f0719 | ||
|
|
4f06f724a4 | ||
|
|
d5a4b266dd | ||
|
|
e1d92a58be | ||
|
|
05ff705c25 | ||
|
|
f1cfb29a03 | ||
|
|
d2f354d949 | ||
|
|
481c6926e7 | ||
|
|
f15da87e13 | ||
|
|
c34aaf7c31 | ||
|
|
fb6815bb7d | ||
|
|
9c78dc76e1 | ||
|
|
62a0a40a49 | ||
|
|
8d61fa17c8 | ||
|
|
a460869d4a | ||
|
|
a9e0d8236c | ||
|
|
fc47276fc3 | ||
|
|
c60334b97b | ||
|
|
36d58d0ff0 | ||
|
|
73529a890b | ||
|
|
b9e88d61a1 | ||
|
|
04bfe71131 | ||
|
|
b16c51825a | ||
|
|
12630dd9f5 | ||
|
|
880b73a3c4 | ||
|
|
63e7ba57bc | ||
|
|
bc2d8a4ce1 | ||
|
|
9f951dbeac | ||
|
|
cba1c23b4d | ||
|
|
d63663a2ea | ||
|
|
41c5ebf1f3 | ||
|
|
757cf0cd13 | ||
|
|
ad8d07cfaa | ||
|
|
d9f7aafd72 | ||
|
|
e0b307d1a8 | ||
|
|
729306f142 | ||
|
|
dc7f745dbe | ||
|
|
8568656d44 | ||
|
|
4dea8849be | ||
|
|
a2056d31bf | ||
|
|
c14344d465 | ||
|
|
b7ba8322d1 | ||
|
|
818a0dac0d | ||
|
|
3f96ba92ce | ||
|
|
b356f217ab | ||
|
|
a968fb0984 | ||
|
|
745d8b80d7 | ||
|
|
12ff3aad93 | ||
|
|
e8501b73a5 | ||
|
|
9c07da6de6 | ||
|
|
8c2e58b67c | ||
|
|
7242494123 | ||
|
|
adfea9f336 | ||
|
|
80d7649dbb | ||
|
|
6e63bb4283 | ||
|
|
ba7a4137fe | ||
|
|
2ca0a4291b | ||
|
|
32a1a35a96 | ||
|
|
df129d8ce3 | ||
|
|
c346da9f6d | ||
|
|
de15a3d05d | ||
|
|
392b42f6f0 | ||
|
|
46a9b587b4 | ||
|
|
1b4d42cc1e | ||
|
|
7c8f6255c5 | ||
|
|
3334a636c5 | ||
|
|
94d52dddda | ||
|
|
b35a73b50f | ||
|
|
7fde6a810d | ||
|
|
843eae1e49 | ||
|
|
8513183684 | ||
|
|
790bebf302 | ||
|
|
0fb76261e8 | ||
|
|
53c69640b7 | ||
|
|
d70cef8ad3 | ||
|
|
a84a23cbcc | ||
|
|
736f2dc657 | ||
|
|
06295fd586 | ||
|
|
e9f4d95dc3 | ||
|
|
223e3b6fbf | ||
|
|
16c967b674 | ||
|
|
a6a1f0621e | ||
|
|
60cb722343 | ||
|
|
2569fe9577 | ||
|
|
be717133ef | ||
|
|
231d585236 | ||
|
|
098faf129c | ||
|
|
0b39ad8341 | ||
|
|
c0117c41e6 | ||
|
|
0ce41e989a | ||
|
|
b6885a0d76 | ||
|
|
125120fcab | ||
|
|
2147a5c3fb | ||
|
|
8c7d5b9585 | ||
|
|
aa4c36885d | ||
|
|
4ee4788378 | ||
|
|
47aea2b12f | ||
|
|
490bc82ee6 | ||
|
|
0d24c636a3 | ||
|
|
5a81b4f375 | ||
|
|
73b90ffb5c | ||
|
|
02e795b265 | ||
|
|
325aa74331 | ||
|
|
1efe2eb329 | ||
|
|
1ba00c7fa8 | ||
|
|
1bfc2fe511 | ||
|
|
1e895f3c8c | ||
|
|
028c283043 | ||
|
|
144ed51100 | ||
|
|
e3c2ec4561 | ||
|
|
84dd957983 | ||
|
|
1093a4f6ad | ||
|
|
c4fdc43aa0 | ||
|
|
15da722af5 | ||
|
|
eec2d2a720 | ||
|
|
1766fad6f7 | ||
|
|
d4775812ad | ||
|
|
6cf887d44d | ||
|
|
bbb2bee2ae | ||
|
|
73763b444e | ||
|
|
9508e01e59 | ||
|
|
375db11e9b | ||
|
|
b1b1972684 | ||
|
|
ce0d4cbc4e | ||
|
|
127d9bc94e | ||
|
|
860df1a898 | ||
|
|
51b36e90f0 | ||
|
|
48b19e149b | ||
|
|
5a87d9dbf5 | ||
|
|
c07b4ba550 | ||
|
|
8a99e3e3fd | ||
|
|
571f54f4e6 | ||
|
|
15cd7b9c13 | ||
|
|
0d21b399b5 | ||
|
|
94ad0bf75c | ||
|
|
2c44286ca5 | ||
|
|
23705727ac | ||
|
|
0a173d230c | ||
|
|
f8987af0e8 | ||
|
|
e046b80bf2 | ||
|
|
f8d6f1d010 | ||
|
|
579190b9ce | ||
|
|
e44e29eb9f | ||
|
|
589cec24e5 | ||
|
|
fd999953f9 | ||
|
|
523dfe7928 | ||
|
|
b2f26e6b1d | ||
|
|
dc45bf3915 | ||
|
|
96e22e25cf | ||
|
|
051beb3c3c | ||
|
|
2ba3d67520 | ||
|
|
cd30d9c1a3 | ||
|
|
7d32aa8276 | ||
|
|
f837b46da1 | ||
|
|
c6107ff694 | ||
|
|
2e7228f88b | ||
|
|
e8825eac5d | ||
|
|
1a88126af8 | ||
|
|
c4a6eba448 | ||
|
|
fc7e9501b2 | ||
|
|
1dfd52db43 | ||
|
|
5510b28656 | ||
|
|
e94abdb159 | ||
|
|
7015607244 | ||
|
|
7ff37d7dcc | ||
|
|
3abc2da106 | ||
|
|
f9c498177a | ||
|
|
872c6483be | ||
|
|
53288e4e9d | ||
|
|
d6b045594c | ||
|
|
aa86111de7 | ||
|
|
040473388e | ||
|
|
f474615729 | ||
|
|
92559e456e | ||
|
|
b2434ea0d0 | ||
|
|
6cf0ce5574 | ||
|
|
518a40f0ba | ||
|
|
387e87bfda | ||
|
|
4fac2a5cd6 | ||
|
|
f5f3ea84d4 | ||
|
|
4b3d470dde | ||
|
|
8513a44e8c | ||
|
|
84b54d97df | ||
|
|
34606f258e | ||
|
|
2c10dd46a0 | ||
|
|
d4c80fc995 | ||
|
|
e1c00f65a5 | ||
|
|
012bc9e8e8 | ||
|
|
a99083107c | ||
|
|
7e93bb0dda | ||
|
|
9735cce043 | ||
|
|
78e1d76f5e | ||
|
|
18b1492d54 | ||
|
|
6116b2fea5 | ||
|
|
6ef8fd2b64 | ||
|
|
9319805d36 | ||
|
|
5027ad37d7 | ||
|
|
70bd0c25c4 | ||
|
|
1a5c7f5437 | ||
|
|
4a9505c334 | ||
|
|
b43ec9ed45 | ||
|
|
eb9c6d542b | ||
|
|
2ec0911821 | ||
|
|
bbb34c8a27 | ||
|
|
1bcb8d6486 | ||
|
|
630b5ca203 | ||
|
|
5b2ed784e1 | ||
|
|
11c9a83ee7 | ||
|
|
2d1b61647a | ||
|
|
fa3797a738 | ||
|
|
fc60c0c980 | ||
|
|
372ca20980 | ||
|
|
b0aad9f1ff | ||
|
|
40e45adbb0 | ||
|
|
5b43a2cee9 | ||
|
|
a319a0daa8 | ||
|
|
79f812d0e1 | ||
|
|
45611a25a5 | ||
|
|
77be659915 | ||
|
|
5986250ed9 | ||
|
|
8156c672b0 | ||
|
|
a443512102 | ||
|
|
4b921319a8 | ||
|
|
6329820a87 | ||
|
|
91e4b0c3d6 | ||
|
|
7666617857 | ||
|
|
19be1090b3 | ||
|
|
354438052e | ||
|
|
b72444b213 | ||
|
|
179078f45c | ||
|
|
1dbc23fe91 | ||
|
|
ff4dec9fea | ||
|
|
6ea51c07b4 | ||
|
|
f4cebb9195 | ||
|
|
cbe5f0dc7c | ||
|
|
0ac8b565b5 | ||
|
|
4f38d4d943 | ||
|
|
0af84eb6b5 | ||
|
|
aa2d19478b | ||
|
|
e035b834a6 | ||
|
|
d2192d609a | ||
|
|
46ad8f495f | ||
|
|
65b2b69a64 | ||
|
|
3220ab6118 |
175
CHANGELOG.md
175
CHANGELOG.md
@@ -5,7 +5,166 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v4.4.0] - 2023-06-30
|
||||
- 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)
|
||||
- Add `auto` that picks `scaled` for mobile devices and `native` elsewhere
|
||||
- Add `resolution-mode` Viewer GET param
|
||||
- Add `PluginConfig.General.ResolutionMode` config item
|
||||
|
||||
## [v4.7.0] - 2024-09-29
|
||||
|
||||
- Add illumination mode
|
||||
- Path-traced SSGI
|
||||
- Automatic thickness (estimate)
|
||||
- Base thickness as max(backface depth) - min(frontface depth)
|
||||
- Per object density factor to adjust thickness
|
||||
- Progressively trace samples to keep viewport interactive
|
||||
- Toggle on/off by pressing "G"
|
||||
- `illumination` Viewer GET param
|
||||
- Enables dXrayShaded define when rendering depth
|
||||
- Fix handling of PDB files that have chains with same id separated by TER record (#1245)
|
||||
- Sequence Panel: Improve visuals of unmodeled sequence positions (#1248)
|
||||
- Fix no-compression xtc parser (#1258)
|
||||
- Mol2 Reader: Fix mol2 status_bit read error (#1251)
|
||||
- Fix shadows with multiple lights
|
||||
- Fix impostor sphere interior normal when using orthographic projection
|
||||
- Add `resolutionMode` parameter to `Canvas3DContext`
|
||||
- `scaled`, divides by `devicePixelRatio`
|
||||
- `native`, no changes
|
||||
- Add `CustomProperty.Context.errorContext` to support reporting errors during loading of custom properties (#1254)
|
||||
- Use in MolViewSpec extension
|
||||
- Mesoscale Explorer: fix color & style issues
|
||||
- Remove use of deprecated SASS explicit color functions
|
||||
- Allow "Components" section to display nested components created by "Apply Action > Selection".
|
||||
|
||||
## [v4.6.0] - 2024-08-28
|
||||
|
||||
- Add round-caps option on tubular alpha helices
|
||||
- Fix missing Sequence UI update on state object removal (#1219)
|
||||
- Improved prmtop format support (CTITLE, %COMMENT)
|
||||
- Avoid calculating bonds for water units when `ignoreHydrogens` is on
|
||||
- Add `Water` trait to `Unit`
|
||||
- Improve entity-id coloring for structures with multiple models from the same source (#1221)
|
||||
- Wrap screenshot & image generation in a `Task`
|
||||
- AlphaFold DB: Add BinaryCIF support when fetching data
|
||||
- PDB-Dev: Add support for 4-character PDB IDs (e.g., 8ZZC)
|
||||
- Fix polymer-gap visual coloring with cartoon theme
|
||||
- Add formal-charge color theme (#328)
|
||||
- Add more coloring options to cartoon theme
|
||||
- Use `CompressionStream` Browser API when available
|
||||
- Add `pdbx_structure_determination_methodology` mmcif field and `Model` helpers
|
||||
- Fix cartoon representation not updated when secondary structure changes
|
||||
- Add Zhang-Skolnick secondary-structure assignment method which handles coarse-grained models (#49)
|
||||
- Calculate bonds for coarse-grained models
|
||||
- VolumeServer: Add `health-check` endpoint + `healthCheckPath` config prop to report service health
|
||||
- ModelServer: Add `health-check` endpoint + `healthCheckPath` config prop to report service health
|
||||
|
||||
## [v4.5.0] - 2024-07-28
|
||||
|
||||
- Separated postprocessing passes
|
||||
- Take into account explicit hydrogens when computing hydrogen bonds
|
||||
- Fix DoF with pixel ratios =! 1
|
||||
- Fix DoF missing transparent depth
|
||||
- Fix trackball pinch zoom and add pan
|
||||
- Fix aromatic link rendering when `adjustCylinderLength` is true
|
||||
- Change trackball animate spin speed unit to radians per second
|
||||
- Fix `mol-plugin-ui/skin/base/components/misc.scss` syntax to be in line with latest Sass syntax
|
||||
- Handle missing theme updates
|
||||
- Fix trajectory-index color-theme not always updated (#896)
|
||||
- Fix bond cylinders not updated on size-theme change with `adjustCylinderLength` enabled (#1215)
|
||||
- Use `OES_texture_float_linear` for SSAO when available
|
||||
|
||||
## [v4.4.1] - 2024-06-30
|
||||
|
||||
- Clean `solidInterior` transparent cylinders
|
||||
- Create a transformer to deflate compressed data
|
||||
@@ -40,14 +199,14 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add `doNotDisposeCanvas3DContext` option to `PluginContext.dispose`
|
||||
- Remove support for density data from edmaps.rcsb.org
|
||||
|
||||
## [v4.3.0] - 2023-05-26
|
||||
## [v4.3.0] - 2024-05-26
|
||||
|
||||
- Fix State Snapshots export animation (#1140)
|
||||
- Add depth of field (dof) postprocessing effect
|
||||
- Add `SbNcbrTunnels` extension for for visualizing tunnels in molecular structures from ChannelsDB (more info in [tunnels.md](./docs/docs/extensions/tunnels.md))
|
||||
- Fix edge case in minimizing RMSD transform computation
|
||||
|
||||
## [v4.2.0] - 2023-05-04
|
||||
## [v4.2.0] - 2024-05-04
|
||||
|
||||
- Add emissive material support
|
||||
- Add bloom post-processing
|
||||
@@ -65,7 +224,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Fix SSAO artifacts (@corredD, #1082)
|
||||
- Fix bumpiness artifacts (#1107, #1084)
|
||||
|
||||
## [v4.1.0] - 2023-03-31
|
||||
## [v4.1.0] - 2024-03-31
|
||||
|
||||
- Add `VolumeTransform` to translate/rotate a volume like in a structure superposition
|
||||
- Fix BinaryCIF encoder edge cases caused by re-encoding an existing BinaryCIF file
|
||||
@@ -76,13 +235,13 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- This can give results similar to pymol's surface_ramp_above_mode=1
|
||||
- Add `rotation` parameter to skybox background
|
||||
|
||||
## [v4.0.1] - 2023-02-19
|
||||
## [v4.0.1] - 2024-02-19
|
||||
|
||||
- Fix BinaryCIF decoder edge cases. Fixes mmCIF model export from data provided by ModelServer.
|
||||
- MolViewSpec extension: support for MVSX file format
|
||||
- Revert "require WEBGL_depth_texture extension" & "remove renderbuffer use"
|
||||
|
||||
## [v4.0.0] - 2023-02-04
|
||||
## [v4.0.0] - 2024-02-04
|
||||
|
||||
- Add Mesoscale Explorer app for investigating large systems
|
||||
- [Breaking] Remove `cellpack` extension (superseded by Mesoscale Explorer app)
|
||||
@@ -118,7 +277,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Add stochastic/dithered transparency to fade overlapping LODs in and out
|
||||
- Add "Automatic Detail" preset that shows surface/cartoon/ball & stick based on camera distance
|
||||
|
||||
## [v3.45.0] - 2023-02-03
|
||||
## [v3.45.0] - 2024-02-03
|
||||
|
||||
- Add color interpolation to impostor cylinders
|
||||
- MolViewSpec components are applicable only when the model has been loaded from MolViewSpec
|
||||
@@ -132,7 +291,7 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
- Support `disableInteractiveUpdates` to only trigger updates once the control loses focus
|
||||
- Move dependencies related to the headless context from optional deps to optional peer deps
|
||||
|
||||
## [v3.44.0] - 2023-01-06
|
||||
## [v3.44.0] - 2024-01-06
|
||||
|
||||
- Add new `cartoon` visuals to support atomic nucleotide base with sugar
|
||||
- Add `thicknessFactor` to `cartoon` representation for scaling nucleotide block/ring/atomic-fill visuals
|
||||
|
||||
@@ -263,6 +263,7 @@ software.version
|
||||
struct.entry_id
|
||||
struct.title
|
||||
struct.pdbx_descriptor
|
||||
struct.pdbx_structure_determination_methodology
|
||||
|
||||
struct_asym.id
|
||||
struct_asym.pdbx_blank_PDB_chainid_flag
|
||||
@@ -367,18 +368,43 @@ struct_site.details
|
||||
|
||||
struct_site_gen.id
|
||||
struct_site_gen.site_id
|
||||
struct_site_gen.pdbx_num_res
|
||||
struct_site_gen.label_comp_id
|
||||
struct_site_gen.auth_asym_id
|
||||
struct_site_gen.auth_atom_id
|
||||
struct_site_gen.auth_comp_id
|
||||
struct_site_gen.auth_seq_id
|
||||
struct_site_gen.details
|
||||
struct_site_gen.label_alt_id
|
||||
struct_site_gen.label_asym_id
|
||||
struct_site_gen.label_atom_id
|
||||
struct_site_gen.label_comp_id
|
||||
struct_site_gen.label_seq_id
|
||||
struct_site_gen.pdbx_auth_ins_code
|
||||
struct_site_gen.auth_comp_id
|
||||
struct_site_gen.auth_asym_id
|
||||
struct_site_gen.auth_seq_id
|
||||
struct_site_gen.label_atom_id
|
||||
struct_site_gen.label_alt_id
|
||||
struct_site_gen.pdbx_num_res
|
||||
struct_site_gen.symmetry
|
||||
struct_site_gen.details
|
||||
|
||||
struct_site_keywords.site_id
|
||||
struct_site_keywords.text
|
||||
|
||||
struct_mon_prot_cis.pdbx_id
|
||||
struct_mon_prot_cis.auth_asym_id
|
||||
struct_mon_prot_cis.auth_comp_id
|
||||
struct_mon_prot_cis.auth_seq_id
|
||||
struct_mon_prot_cis.label_alt_id
|
||||
struct_mon_prot_cis.label_asym_id
|
||||
struct_mon_prot_cis.label_comp_id
|
||||
struct_mon_prot_cis.label_seq_id
|
||||
struct_mon_prot_cis.pdbx_PDB_ins_code
|
||||
struct_mon_prot_cis.pdbx_PDB_ins_code_2
|
||||
struct_mon_prot_cis.pdbx_PDB_model_num
|
||||
struct_mon_prot_cis.pdbx_auth_asym_id_2
|
||||
struct_mon_prot_cis.pdbx_auth_comp_id_2
|
||||
struct_mon_prot_cis.pdbx_auth_ins_code
|
||||
struct_mon_prot_cis.pdbx_auth_ins_code_2
|
||||
struct_mon_prot_cis.pdbx_auth_seq_id_2
|
||||
struct_mon_prot_cis.pdbx_label_asym_id_2
|
||||
struct_mon_prot_cis.pdbx_label_comp_id_2
|
||||
struct_mon_prot_cis.pdbx_label_seq_id_2
|
||||
struct_mon_prot_cis.pdbx_omega_angle
|
||||
|
||||
symmetry.entry_id
|
||||
symmetry.cell_setting
|
||||
@@ -850,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
|
||||
|
||||
|
@@ -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]
|
||||
|
||||
@@ -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 `<maximum number of voxel> = maxRequestBlockCount * <block size>^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: `<response gzipped size>` in `[<voxel count> / 8, <voxel count> / 4]`. The maximum number of voxels is tied to maxRequestBlockCount. |
|
||||
|
||||
59
docs/docs/misc/exporting-components.md
Normal file
59
docs/docs/misc/exporting-components.md
Normal 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))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -22,7 +22,7 @@
|
||||
* Discontinuous chains, i.e. gaps in the sequence (3sn6)
|
||||
* Lots of sheets (1cbs)
|
||||
* DNA (2np2, 1d66)
|
||||
* C-alpha only (2rcj)
|
||||
* C-alpha only (2RCJ, 6ZIG, 5AJ2)
|
||||
* Not cyclic, but termini are backbone-only and within distance but seqIds are not compatible (6SW3)
|
||||
* Close backbone atoms but not linked (e.g. 4HIV)
|
||||
* Non-standard residues
|
||||
|
||||
193
docs/docs/plugin/custom-library.md
Normal file
193
docs/docs/plugin/custom-library.md
Normal file
@@ -0,0 +1,193 @@
|
||||
# Building a Custom Library
|
||||
|
||||
This page goes over creating a custom Mol\* based library usable inside a `<script>` tag in an HTML page.
|
||||
|
||||
## Setup
|
||||
|
||||
- Create a new npm/yarn package
|
||||
- Install `molstar` and `esbuild` packages
|
||||
|
||||
```
|
||||
mkdir molstar-lib
|
||||
cd molstar-lib
|
||||
npm init
|
||||
npm install molstar
|
||||
npm install esbuild --save-dev
|
||||
```
|
||||
|
||||
## Example Library Code
|
||||
|
||||
Create new file `src/index.ts` (or `.js` if you don't want to use TypeScript):
|
||||
|
||||
```ts
|
||||
import { DefaultPluginSpec, PluginSpec } from 'molstar/lib/mol-plugin/spec';
|
||||
import { PluginContext } from 'molstar/lib/mol-plugin/context';
|
||||
|
||||
export async function initViewer(element: string | HTMLDivElement, options?: { spec?: PluginSpec }) {
|
||||
const parent = typeof element === 'string' ? document.getElementById(element)! as HTMLDivElement : element;
|
||||
const canvas = document.createElement('canvas') as HTMLCanvasElement;
|
||||
parent.appendChild(canvas);
|
||||
|
||||
const spec = options?.spec ?? DefaultPluginSpec();
|
||||
|
||||
const plugin = new PluginContext(spec);
|
||||
await plugin.init();
|
||||
|
||||
plugin.initViewer(canvas, parent);
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
export async function loadStructure(
|
||||
plugin: PluginContext,
|
||||
url: string,
|
||||
options?: { format?: string, isBinary?: boolean }
|
||||
) {
|
||||
const data = await plugin.builders.data.download(
|
||||
{ url, isBinary: options?.isBinary }
|
||||
);
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(
|
||||
data,
|
||||
options?.format ?? 'mmcif' as any
|
||||
);
|
||||
const preset = await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
return preset;
|
||||
}
|
||||
```
|
||||
|
||||
## Building the Library
|
||||
|
||||
Add new commands to the `scripts` section of the `package.json` file
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
"build": "esbuild src/index.ts --bundle --outfile=./build/js/index.js --global-name=molstarLib",
|
||||
"watch": "esbuild src/index.ts --bundle --outfile=./build/js/index.js --global-name=molstarLib --watch"
|
||||
}
|
||||
```
|
||||
|
||||
and run the command `npm run build` (or `watch` for interactive development experience). This will create `build/js/index.js` file which can be imported with a `<script>` tag and the exported functions called view the `molstarLib` prefix (you can customize this parameter).
|
||||
|
||||
## Using the Library
|
||||
|
||||
Create file `build/index.html`:
|
||||
|
||||
```html
|
||||
<!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* Library Example</title>
|
||||
</head>
|
||||
<style>
|
||||
#viewer {
|
||||
position: absolute;
|
||||
width: 800px;
|
||||
height: 600px;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="./js/index.js"></script>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<script type="text/javascript">
|
||||
async function init() {1
|
||||
const plugin = await molstarLib.initViewer("viewer");
|
||||
await molstarLib.loadStructure(
|
||||
plugin,
|
||||
"https://models.rcsb.org/4hhb.bcif",
|
||||
{ isBinary: true }
|
||||
);
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
After opening `index.html` in a browser, you should see
|
||||
|
||||

|
||||
|
||||
## Using Mol* React UI
|
||||
|
||||
The above example does not make use of the default Mol\* React UI and any UI components are therefore the author's responsibility. The below examples show how to (re)use the Mol\* React UI.
|
||||
|
||||
- Create `src/ui.tsx`:
|
||||
```tsx
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { DefaultPluginUISpec, PluginUISpec } from 'molstar/lib/mol-plugin-ui/spec';
|
||||
import { PluginUIContext } from 'molstar/lib/mol-plugin-ui/context';
|
||||
import { Plugin } from 'molstar/lib/mol-plugin-ui/plugin';
|
||||
|
||||
export async function initViewerUI(element: string | HTMLDivElement, options?: { spec?: PluginUISpec }) {
|
||||
const parent = typeof element === 'string' ? document.getElementById(element)! as HTMLDivElement : element;
|
||||
const spec = { ...DefaultPluginUISpec(), ...options?.spec };
|
||||
const plugin = new PluginUIContext(spec);
|
||||
await plugin.init();
|
||||
|
||||
createRoot(parent).render(<Plugin plugin={plugin} />)
|
||||
|
||||
return plugin;
|
||||
}
|
||||
|
||||
export async function loadStructure(plugin: PluginUIContext, url: string, options?: { format?: string, isBinary?: boolean }) {
|
||||
const data = await plugin.builders.data.download({ url, isBinary: options?.isBinary });
|
||||
const trajectory = await plugin.builders.structure.parseTrajectory(data, options?.format ?? 'mmcif' as any);
|
||||
await plugin.builders.structure.hierarchy.applyPreset(trajectory, 'default');
|
||||
}
|
||||
```
|
||||
- Create `src/style.scss`:
|
||||
```scss
|
||||
@import '../node_modules/molstar/lib/mol-plugin-ui/skin/light.scss';
|
||||
```
|
||||
- Create `build/ui.html`:
|
||||
```html
|
||||
<!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* UI Library Example</title>
|
||||
</head>
|
||||
<link rel="stylesheet" type="text/css" href="css/style.css" />
|
||||
<style>
|
||||
#viewer {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
</style>
|
||||
<script type="text/javascript" src="./js/ui.js"></script>
|
||||
<body>
|
||||
<div id="viewer"></div>
|
||||
<script type="text/javascript">
|
||||
async function init() {
|
||||
const plugin = await molstarLib.initViewerUI("viewer", {
|
||||
spec: {
|
||||
layout: {
|
||||
initial: {
|
||||
isExpanded: true,
|
||||
showControls: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
});
|
||||
await molstarLib.loadStructure(plugin, "https://models.rcsb.org/4hhb.bcif", { isBinary: true });
|
||||
}
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
- Install `sass`: `npm install sass -save-dev` (or use [`esbuild` plugin](https://www.npmjs.com/package/esbuild-sass-plugin) and `import` the scss file in `ui.tsx`)
|
||||
- Add scripts to `package.json`:
|
||||
```json
|
||||
"build-ui": "esbuild src/ui.tsx --bundle --outfile=./build/js/ui.js --global-name=molstarLib",
|
||||
"css": "sass src/style.scss ./build/css/style.css"
|
||||
```
|
||||
- Run `npm run build-ui` and `npm run css` (skip if using `esbuild-sass-plugin`)
|
||||
- Opening `build/ui.html`:
|
||||

|
||||
@@ -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`.
|
||||
|
||||
|
||||
|
||||
BIN
docs/docs/plugin/lib-example.png
Normal file
BIN
docs/docs/plugin/lib-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 255 KiB |
45
docs/docs/plugin/transforms/custom-conformation.md
Normal file
45
docs/docs/plugin/transforms/custom-conformation.md
Normal 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');
|
||||
```
|
||||
BIN
docs/docs/plugin/ui-example.png
Normal file
BIN
docs/docs/plugin/ui-example.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 420 KiB |
@@ -32,6 +32,7 @@ nav:
|
||||
- Plugin:
|
||||
- Creating Instance: 'plugin/instance.md'
|
||||
- Examples: plugin/examples.md
|
||||
- Custom Library: 'plugin/custom-library.md'
|
||||
- Selections: 'plugin/selections.md'
|
||||
- Viewer State: 'plugin/viewer-state.md'
|
||||
- Data State: 'plugin/data-state.md'
|
||||
@@ -39,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:
|
||||
@@ -56,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
|
||||
|
||||
4179
package-lock.json
generated
4179
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
71
package.json
71
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.4.0",
|
||||
"version": "4.10.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -108,79 +108,88 @@
|
||||
"Cai Huiyu <szmun.caihy@gmail.com>",
|
||||
"Ryan DiRisio <rjdiris@gmail.com>",
|
||||
"Dušan Veľký <dvelky@mail.muni.cz>",
|
||||
"Neli Fonseca <neli@ebi.ac.uk>"
|
||||
"Neli Fonseca <neli@ebi.ac.uk>",
|
||||
"Paul Pillot <paul.pillot@tandemai.com>",
|
||||
"Herman Bergwerf <post@hbergwerf.nl>",
|
||||
"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.14",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.14.1",
|
||||
"@typescript-eslint/parser": "^7.14.1",
|
||||
"@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": "^8.2.2",
|
||||
"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.0",
|
||||
"eslint": "^8.57.1",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.2.0",
|
||||
"http-server": "^14.1.1",
|
||||
"jest": "^29.7.0",
|
||||
"jpeg-js": "^0.4.4",
|
||||
"mini-css-extract-plugin": "^2.9.0",
|
||||
"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.77.6",
|
||||
"sass-loader": "^14.2.1",
|
||||
"simple-git": "^3.25.0",
|
||||
"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.1.5",
|
||||
"typescript": "^5.5.2",
|
||||
"webpack": "^5.92.1",
|
||||
"ts-jest": "^29.2.5",
|
||||
"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.39",
|
||||
"@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.2",
|
||||
"compression": "^1.7.4",
|
||||
"compression": "^1.7.5",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^4.19.2",
|
||||
"express": "^5.0.1",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^4.3.6",
|
||||
"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.6.3",
|
||||
"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
|
||||
},
|
||||
|
||||
@@ -42,7 +42,7 @@ function copyViewer() {
|
||||
function copyMe() {
|
||||
console.log('\n###', 'copy me files');
|
||||
const meBuildPath = path.resolve(buildDir, '../build/mesoscale-explorer/');
|
||||
const meDeployPath = path.resolve(localPath, 'me/');
|
||||
const meDeployPath = path.resolve(localPath, 'me/viewer/');
|
||||
fse.copySync(meBuildPath, meDeployPath, { overwrite: true });
|
||||
addAnalytics(path.resolve(meDeployPath, 'index.html'));
|
||||
}
|
||||
@@ -61,9 +61,13 @@ function copyDemos() {
|
||||
}
|
||||
|
||||
function copyFiles() {
|
||||
copyViewer();
|
||||
copyMe();
|
||||
copyDemos();
|
||||
try {
|
||||
copyViewer();
|
||||
copyMe();
|
||||
copyDemos();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs.existsSync(localPath)) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -47,6 +47,7 @@ export type ExampleEntry = {
|
||||
export type MesoscaleExplorerState = {
|
||||
examples?: ExampleEntry[],
|
||||
graphicsMode: GraphicsMode,
|
||||
illumination: boolean,
|
||||
stateRef?: string,
|
||||
driver?: any,
|
||||
stateCache: { [k: string]: any },
|
||||
@@ -78,6 +79,8 @@ const DefaultMesoscaleExplorerOptions = {
|
||||
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
|
||||
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
|
||||
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
@@ -185,6 +188,7 @@ export class MesoscaleExplorer {
|
||||
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.General.ResolutionMode, o.resolutionMode],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
@@ -213,9 +217,9 @@ export class MesoscaleExplorer {
|
||||
onBeforeUIRender: async plugin => {
|
||||
let examples: MesoscaleExplorerState['examples'] = undefined;
|
||||
try {
|
||||
examples = await plugin.fetch({ url: './examples/list.json', type: 'json' }).run();
|
||||
examples = await plugin.fetch({ url: '../examples/list.json', type: 'json' }).run();
|
||||
// extend the array with file tour.json if it exists
|
||||
const tour = await plugin.fetch({ url: './examples/tour.json', type: 'json' }).run();
|
||||
const tour = await plugin.fetch({ url: '../examples/tour.json', type: 'json' }).run();
|
||||
if (tour) {
|
||||
examples = examples?.concat(tour);
|
||||
}
|
||||
@@ -226,6 +230,7 @@ export class MesoscaleExplorer {
|
||||
(plugin.customState as MesoscaleExplorerState) = {
|
||||
examples,
|
||||
graphicsMode: o.graphicsMode,
|
||||
illumination: o.illumination,
|
||||
driver: o.driver,
|
||||
stateCache: {},
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
|
||||
import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../mol-state';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { CellpackAssembly, CellpackStructure } from './model';
|
||||
|
||||
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, merge?: boolean) {
|
||||
@@ -96,12 +96,12 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
|
||||
const compRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'comp:', label: 'compartment', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const funcRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'func:', label: 'function', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
if (entities._rowCount > 1) {
|
||||
@@ -159,7 +159,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
parent.cell!.state.isCollapsed = false;
|
||||
const group = await state.build()
|
||||
.to(parent)
|
||||
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:comp:${n}`, `comp:${p}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
compGroups.set(n, group);
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
const color = colorIdx !== undefined ? baseFuncColors[colorIdx] : ColorNames.white;
|
||||
const group = await state.build()
|
||||
.to(funcRoot)
|
||||
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:func:${f}`, 'func:'], state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
funcGroups.set(f, group);
|
||||
}
|
||||
@@ -201,9 +201,6 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode), { tags: [`comp:${n}`, `func:${f}`] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { utf8Read } from '../../../../mol-io/common/utf8';
|
||||
import { Mat3, Quat, Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { ShapeRepresentation3D, StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { ParseCif, ParsePly, ReadFile } from '../../../../mol-plugin-state/transforms/data';
|
||||
@@ -250,10 +250,6 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
}
|
||||
}
|
||||
await build.commit();
|
||||
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../m
|
||||
import { Clip } from '../../../../mol-util/clip';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { MmcifAssembly, MmcifStructure } from './model';
|
||||
|
||||
function getSpacefillParams(color: Color, scaleFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
|
||||
@@ -114,7 +114,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
|
||||
const entRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: 'ent:', label: 'entity', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const getEntityType = (i: number) => {
|
||||
@@ -172,9 +172,6 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(color, scaleFactor, graphicsMode, clipVariant), { tags: [`ent:${t}`] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
|
||||
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
|
||||
import { Task } from '../../../../mol-task';
|
||||
@@ -97,12 +97,12 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
|
||||
|
||||
const group = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: ['group:ent:'], state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
|
||||
await state.build()
|
||||
.to(group)
|
||||
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', illustrative: false, value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', illustrative: false, value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: ['group:ent:mem', 'ent:', '__no_group_color__'], state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const colors = getDistinctBaseColors(other.length, 0);
|
||||
@@ -115,18 +115,15 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
|
||||
build = build
|
||||
.to(cell)
|
||||
.apply(StructureFromPetworld, membrane[i])
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgrey, graphicsMode), { tags: [`ent:mem`] });
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(ColorNames.lightgrey, graphicsMode), { tags: ['ent:mem', '__no_group_color__'] });
|
||||
}
|
||||
for (let i = 0, il = other.length; i < il; ++i) {
|
||||
build = build
|
||||
.to(cell)
|
||||
.apply(StructureFromPetworld, other[i])
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: [`ent:`] });
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: ['ent:'] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -339,10 +339,10 @@ export function getLodLevels(graphicsMode: Exclude<GraphicsMode, 'custom'>): Lod
|
||||
];
|
||||
case 'ultra':
|
||||
return [
|
||||
{ minDistance: 1, maxDistance: 2000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 2000, maxDistance: 8000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 8000, maxDistance: 20000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 20000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
{ minDistance: 1, maxDistance: 5000, overlap: 0, stride: 1, scaleBias: 1 },
|
||||
{ minDistance: 5000, maxDistance: 10000, overlap: 0, stride: 10, scaleBias: 3 },
|
||||
{ minDistance: 10000, maxDistance: 30000, overlap: 0, stride: 50, scaleBias: 2.5 },
|
||||
{ minDistance: 30000, maxDistance: 10000000, overlap: 0, stride: 200, scaleBias: 2 },
|
||||
];
|
||||
default:
|
||||
assertUnreachable(graphicsMode);
|
||||
@@ -471,7 +471,7 @@ export function getAllGroups(plugin: PluginContext, tag?: string) {
|
||||
return _getAllGroups(plugin, tag, []);
|
||||
}
|
||||
|
||||
export function getAllLeafGroups(plugin: PluginContext, tag: string | undefined) {
|
||||
export function getAllLeafGroups(plugin: PluginContext, tag: string) {
|
||||
const allGroups = getAllGroups(plugin, tag);
|
||||
allGroups.sort((a, b) => a.params?.values.index - b.params?.values.index);
|
||||
return allGroups.filter(g => {
|
||||
@@ -505,7 +505,7 @@ function getFilterMatcher(filter: string) {
|
||||
: new RegExp(escapeRegExp(filter), 'gi');
|
||||
}
|
||||
|
||||
export function getFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
|
||||
export function getFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
|
||||
if (!filter) return getEntities(plugin, tag);
|
||||
const matcher = getFilterMatcher(filter);
|
||||
return getEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
|
||||
@@ -523,7 +523,7 @@ export function getAllEntities(plugin: PluginContext, tag?: string) {
|
||||
return _getAllEntities(plugin, tag, []);
|
||||
}
|
||||
|
||||
export function getAllFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
|
||||
export function getAllFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
|
||||
if (!filter) return getAllEntities(plugin, tag);
|
||||
const matcher = getFilterMatcher(filter);
|
||||
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
|
||||
@@ -553,14 +553,43 @@ export function getEntityDescription(plugin: PluginContext, cell: StateObjectCel
|
||||
return d;
|
||||
}
|
||||
|
||||
export async function updateStyle(plugin: PluginContext, options: { ignoreLight: boolean, material: Material, celShaded: boolean, illustrative: boolean }) {
|
||||
const update = plugin.state.data.build();
|
||||
const { ignoreLight, material, celShaded, illustrative } = options;
|
||||
|
||||
export async function updateColors(plugin: PluginContext, values: PD.Values, options?: PD.Values, tag?: string, filter?: string) {
|
||||
const entities = getAllEntities(plugin);
|
||||
|
||||
for (let j = 0; j < entities.length; ++j) {
|
||||
update.to(entities[j]).update(old => {
|
||||
if (old.type) {
|
||||
const value = old.colorTheme.name === 'illustrative'
|
||||
? old.colorTheme.params.style.params.value
|
||||
: old.colorTheme.params.value;
|
||||
const lightness = old.colorTheme.name === 'illustrative'
|
||||
? old.colorTheme.params.style.params.lightness
|
||||
: old.colorTheme.params.lightness;
|
||||
if (illustrative) {
|
||||
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value, lightness } } } };
|
||||
} else {
|
||||
old.colorTheme = { name: 'uniform', params: { value, lightness } };
|
||||
}
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await update.commit();
|
||||
};
|
||||
|
||||
export async function updateColors(plugin: PluginContext, values: PD.Values, tag: string, filter: string) {
|
||||
const update = plugin.state.data.build();
|
||||
const { type, illustrative, value, shift, lightness, alpha, emissive } = values;
|
||||
const doLighting = (options !== undefined);
|
||||
const { ignoreLight, materialStyle: material, celShaded } = options ? options : { ignoreLight: true, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 }, celShaded: false };
|
||||
if (type === 'group-generate' || type === 'group-uniform') {
|
||||
const groups = getAllLeafGroups(plugin, tag);
|
||||
const leafGroups = getAllLeafGroups(plugin, tag);
|
||||
const rootLeafGroups = getRoots(plugin).filter(g => g.params?.values.tag === tag && getEntities(plugin, g.params?.values.tag).length > 0);
|
||||
const groups = [...leafGroups, ...rootLeafGroups];
|
||||
const baseColors = getDistinctBaseColors(groups.length, shift);
|
||||
|
||||
for (let i = 0; i < groups.length; ++i) {
|
||||
@@ -585,11 +614,6 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, opt
|
||||
old.type.params.alpha = alpha;
|
||||
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
|
||||
old.type.params.emissive = emissive;
|
||||
if (doLighting) {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = c;
|
||||
old.coloring.params.lightness = lightness;
|
||||
@@ -629,11 +653,6 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, opt
|
||||
old.type.params.alpha = alpha;
|
||||
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
|
||||
old.type.params.emissive = emissive;
|
||||
if (doLighting) {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = c;
|
||||
old.coloring.params.lightness = lightness;
|
||||
@@ -668,16 +687,3 @@ export function expandAllGroups(plugin: PluginContext) {
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateReprParams(plugin: PluginContext, options: PD.Values) {
|
||||
const update = plugin.state.data.build();
|
||||
const { ignoreLight, materialStyle: material, celShaded } = options;
|
||||
const entities = getAllEntities(plugin);
|
||||
for (let j = 0; j < entities.length; ++j) {
|
||||
update.to(entities[j]).update(old => {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
});
|
||||
}
|
||||
await update.commit();
|
||||
}
|
||||
79
src/apps/mesoscale-explorer/embedded.html
Normal file
79
src/apps/mesoscale-explorer/embedded.html
Normal file
@@ -0,0 +1,79 @@
|
||||
<!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">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html, body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
overflow: hidden;
|
||||
}
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 800px;
|
||||
margin-bottom: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
button {
|
||||
margin: 5px;
|
||||
padding: 10px 20px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#viewer-container {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border: 1px solid #ccc;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" type="text/css" href="./molstar.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="controls">
|
||||
<button onclick="loadExample('cellpack-hiv1')">Load HIV-1 Example</button>
|
||||
<button onclick="loadExample('machineryoflife-tour')">Load Machinery of Life Tour</button>
|
||||
<button onclick="loadExample('petworld-synvesicle')">Load Synaptic Vesicle Example</button>
|
||||
</div>
|
||||
<div id="viewer-container">
|
||||
<div id="meso-viewer" style="position: relative; width: 100%; height: 400px;"></div>
|
||||
</div>
|
||||
|
||||
<script src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
let mesoExplorer;
|
||||
|
||||
function loadExample(example) {
|
||||
if (mesoExplorer) {
|
||||
mesoExplorer.loadExample(example);
|
||||
}
|
||||
}
|
||||
|
||||
molstar.MesoscaleExplorer.create('meso-viewer', {
|
||||
layoutShowControls: false,
|
||||
viewportShowExpand: false,
|
||||
layoutIsExpanded: false,
|
||||
powerPreference: 'high-performance',
|
||||
graphicsMode: 'quality'
|
||||
}).then(me => {
|
||||
mesoExplorer = me;
|
||||
me.loadExample('cellpack-hiv1'); // Load the default example on page load
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
me.dispose();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="./extras/driver.css"/>
|
||||
<link rel="stylesheet" href="../extras/driver.css"/>
|
||||
<title>Mol* Mesoscale Explorer</title>
|
||||
<style>
|
||||
* {
|
||||
@@ -39,7 +39,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./extras/driver.js.iife.js"></script>
|
||||
<script src="../extras/driver.js.iife.js"></script>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
const driver = window.driver ? window.driver.js.driver() : undefined;
|
||||
@@ -60,6 +60,8 @@
|
||||
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
var graphicsMode = getParam('graphics-mode', '[^&]+').trim().toLowerCase();
|
||||
var illumination = getParam('illumination', '[^&]+').trim() === '1';
|
||||
var resolutionMode = getParam('resolution-mode', '[^&]+').trim().toLowerCase();
|
||||
|
||||
molstar.MesoscaleExplorer.create('app', {
|
||||
layoutShowControls: !hideControls,
|
||||
@@ -68,6 +70,8 @@
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
graphicsMode: graphicsMode || 'quality',
|
||||
illumination: illumination,
|
||||
resolutionMode: resolutionMode || 'auto',
|
||||
driver: driver
|
||||
}).then(me => {
|
||||
var example = getParam('example', '[^&]+').trim();
|
||||
@@ -94,7 +98,6 @@
|
||||
me.loadPdbDev(pdbdev);
|
||||
return;
|
||||
}
|
||||
|
||||
window.addEventListener('unload', () => {
|
||||
// to aid GC
|
||||
me.dispose();
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@use "sass:color";
|
||||
|
||||
$default-background: #2D3E50;
|
||||
$font-color: #EDF1F2;
|
||||
$hover-font-color: #3B9AD9;
|
||||
@@ -16,11 +18,11 @@ $log-error: #FD354B;
|
||||
$logo-background: rgba(0,0,0,0.75);
|
||||
|
||||
@function color-lower-contrast($color, $amount) {
|
||||
@return darken($color, $amount);
|
||||
@return color.adjust($color, $lightness: -$amount, $space: hsl);
|
||||
}
|
||||
|
||||
@function color-increase-contrast($color, $amount) {
|
||||
@return lighten($color, $amount);
|
||||
@return color.adjust($color, $lightness: $amount, $space: hsl);
|
||||
}
|
||||
|
||||
@import 'mol-plugin-ui/skin/base/base';
|
||||
@@ -35,7 +37,7 @@ a {
|
||||
|
||||
|
||||
.msp-snapshot-description-me {
|
||||
background: rgba(red($default-background), green($default-background), blue($default-background), 0.5);
|
||||
background: color.change($default-background, $alpha: 0.5, $space: rgb);
|
||||
|
||||
position: absolute;
|
||||
height: 50vh; // 50% of the viewport height
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
@@ -749,7 +752,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
};
|
||||
|
||||
updateRoot = async (values: PD.Values) => {
|
||||
await updateColors(this.plugin, values, undefined, this.cell.params?.values.tag, this.props.filter);
|
||||
await updateColors(this.plugin, values, this.cell.params?.values.tag, this.props.filter);
|
||||
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import Markdown from 'react-markdown';
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
|
||||
import { StructureComponentManager } from '../../../mol-plugin-state/manager/structure/component';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
@@ -26,9 +24,13 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateColors } from '../data/state';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateStyle } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
|
||||
function adjustPluginProps(ctx: PluginContext) {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
|
||||
ctx.managers.interactivity.setProps({ granularity: 'chain' });
|
||||
ctx.canvas3d?.setProps({
|
||||
multiSample: { mode: 'off' },
|
||||
@@ -82,12 +84,12 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
transparentThreshold: 0.4,
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.6,
|
||||
maxDistance: 80,
|
||||
steps: 3,
|
||||
tolerance: 1.0,
|
||||
@@ -101,8 +103,13 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
color: Color(0x000000),
|
||||
includeTransparent: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
illumination: {
|
||||
enabled: customState.illumination,
|
||||
firstStepSize: 0.1,
|
||||
rayDistance: 1024,
|
||||
},
|
||||
});
|
||||
|
||||
const { graphics } = MesoscaleState.get(ctx);
|
||||
@@ -165,14 +172,36 @@ export async function loadExampleEntry(ctx: PluginContext, entry: ExampleEntry)
|
||||
}
|
||||
|
||||
export async function loadUrl(ctx: PluginContext, url: string, type: 'molx' | 'molj' | 'cif' | 'bcif') {
|
||||
let startTime = 0;
|
||||
if (isTimingMode) {
|
||||
startTime = now();
|
||||
}
|
||||
if (type === 'molx' || type === 'molj') {
|
||||
const customState = ctx.customState as MesoscaleExplorerState;
|
||||
delete customState.stateRef;
|
||||
customState.stateCache = {};
|
||||
ctx.managers.asset.clear();
|
||||
|
||||
await PluginCommands.State.Snapshots.Clear(ctx);
|
||||
await PluginCommands.State.Snapshots.OpenUrl(ctx, { url, type });
|
||||
|
||||
const cell = ctx.state.data.selectQ(q => q.ofType(MesoscaleStateObject))[0];
|
||||
if (!cell) throw new Error('Missing MesoscaleState');
|
||||
|
||||
customState.stateRef = cell.transform.ref;
|
||||
customState.graphicsMode = cell.obj?.data.graphics || customState.graphicsMode;
|
||||
} else {
|
||||
await reset(ctx);
|
||||
const isBinary = type === 'bcif';
|
||||
const data = await ctx.builders.data.download({ url, isBinary });
|
||||
await createHierarchy(ctx, data.ref);
|
||||
}
|
||||
if (isTimingMode) {
|
||||
const endTime = now();
|
||||
// Calculate the elapsed time
|
||||
const timeTaken = endTime - startTime;
|
||||
console.log(`Model loaded in ${timeTaken} milliseconds`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadPdb(ctx: PluginContext, id: string) {
|
||||
@@ -184,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);
|
||||
}
|
||||
@@ -421,7 +456,7 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
|
||||
openHelp = () => {
|
||||
// open a new page with the documentation
|
||||
window.open('https://molstar.org/me/docs', '_blank');
|
||||
window.open('https://molstar.org/me-docs/', '_blank');
|
||||
};
|
||||
|
||||
toggleHelp = () => {
|
||||
@@ -440,12 +475,11 @@ export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, s
|
||||
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
|
||||
if (!driver) return;
|
||||
|
||||
const legend = `## Welcome to Mol* Mesoscale Explorer`;
|
||||
const help = <IconButton svg={HelpOutlineSvg} toggleState={false} small onClick={this.openHelp} title='Open the Documentation' />;
|
||||
const tour = <IconButton svg={TourSvg} toggleState={false} small onClick={this.toggleHelp} title='Start the interactive tour' />;
|
||||
return <>
|
||||
<div id='explorerinfo' style={{ paddingLeft: 4 }} className='msp-help-text'>
|
||||
<Markdown>{legend}</Markdown>
|
||||
<div id='explorerinfo' style={{ display: 'flex', alignItems: 'center', padding: '4px 0 4px 8px' }} className='msp-help-text'>
|
||||
<h2 style={{ flexGrow: 1 }}>Mol* Mesoscale Explorer</h2>
|
||||
{tour}{help}
|
||||
</div>
|
||||
</>;
|
||||
@@ -470,65 +504,18 @@ export class MesoQuickStylesControls extends CollapsableControls {
|
||||
}
|
||||
|
||||
export class MesoQuickStyles extends PluginUIComponent {
|
||||
state = {
|
||||
celShaded: false,
|
||||
};
|
||||
default_color_values = {
|
||||
type: 'group-generate',
|
||||
illustrative: false,
|
||||
value: [1, 1, 1, 1],
|
||||
variability: 20,
|
||||
shift: 0,
|
||||
lightness: 0,
|
||||
alpha: 1,
|
||||
emissive: 0
|
||||
};
|
||||
illustrative_color_values = {
|
||||
type: 'group-generate',
|
||||
illustrative: true,
|
||||
value: [1, 1, 1, 1],
|
||||
variability: 20,
|
||||
shift: 0,
|
||||
lightness: 0,
|
||||
alpha: 1,
|
||||
emissive: 0
|
||||
};
|
||||
async default() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.6,
|
||||
maxDistance: 80,
|
||||
steps: 3,
|
||||
tolerance: 1.0,
|
||||
@@ -546,49 +533,26 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
|
||||
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: true,
|
||||
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
|
||||
celShaded: false,
|
||||
illustrative: false,
|
||||
});
|
||||
}
|
||||
|
||||
async celshading() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.5,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
@@ -598,49 +562,26 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: true, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: false,
|
||||
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
|
||||
celShaded: true,
|
||||
illustrative: false,
|
||||
});
|
||||
}
|
||||
|
||||
async stylizedDof() {
|
||||
async shinyDof() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
@@ -660,49 +601,26 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
}
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: false,
|
||||
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
|
||||
celShaded: false,
|
||||
illustrative: false,
|
||||
});
|
||||
}
|
||||
|
||||
async illustrative() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.5,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
@@ -720,93 +638,48 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.illustrative_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: true,
|
||||
material: { metalness: 0, roughness: 1.0, bumpiness: 0 },
|
||||
celShaded: false,
|
||||
illustrative: true,
|
||||
});
|
||||
}
|
||||
|
||||
async shiny() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} },
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: false,
|
||||
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
|
||||
celShaded: false,
|
||||
illustrative: false,
|
||||
});
|
||||
}
|
||||
|
||||
async stylized() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
const p = this.plugin.canvas3d.props;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
...p.postprocessing,
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
@@ -824,11 +697,12 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.illustrative_color_values, options);
|
||||
await updateStyle(this.plugin, {
|
||||
ignoreLight: false,
|
||||
material: { metalness: 0, roughness: 0.2, bumpiness: 0 },
|
||||
celShaded: false,
|
||||
illustrative: true,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -851,7 +725,7 @@ export class MesoQuickStyles extends PluginUIComponent {
|
||||
<Button noOverflow title='Enable shiny material, outline, and illustrative colors' onClick={() => this.stylized()} style={{ width: 'auto' }}>
|
||||
Shiny-Illustrative
|
||||
</Button>
|
||||
<Button noOverflow title='Enable DOF and shiny material' onClick={() => this.stylizedDof()} style={{ width: 'auto' }}>
|
||||
<Button noOverflow title='Enable DOF and shiny material' onClick={() => this.shinyDof()} style={{ width: 'auto' }}>
|
||||
Shiny-DOF
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
@@ -105,6 +106,8 @@ const DefaultViewerOptions = {
|
||||
preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
|
||||
allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
|
||||
powerPreference: PluginConfig.General.PowerPreference.defaultValue,
|
||||
resolutionMode: PluginConfig.General.ResolutionMode.defaultValue,
|
||||
illumination: false,
|
||||
|
||||
viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
|
||||
viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
|
||||
@@ -122,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;
|
||||
|
||||
@@ -182,6 +187,7 @@ export class Viewer {
|
||||
[PluginConfig.General.PreferWebGl1, o.preferWebgl1],
|
||||
[PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
|
||||
[PluginConfig.General.PowerPreference, o.powerPreference],
|
||||
[PluginConfig.General.ResolutionMode, o.resolutionMode],
|
||||
[PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
|
||||
[PluginConfig.Viewport.ShowControls, o.viewportShowControls],
|
||||
[PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
|
||||
@@ -200,6 +206,7 @@ export class Viewer {
|
||||
[AssemblySymmetryConfig.DefaultServerType, o.rcsbAssemblySymmetryDefaultServerType],
|
||||
[AssemblySymmetryConfig.DefaultServerUrl, o.rcsbAssemblySymmetryDefaultServerUrl],
|
||||
[AssemblySymmetryConfig.ApplyColors, o.rcsbAssemblySymmetryApplyColors],
|
||||
...(o.config ?? []),
|
||||
]
|
||||
};
|
||||
|
||||
@@ -217,6 +224,7 @@ export class Viewer {
|
||||
plugin.builders.structure.representation.registerPreset(ViewerAutoPreset);
|
||||
}
|
||||
});
|
||||
plugin.canvas3d?.setProps({ illumination: { enabled: o.illumination } });
|
||||
return new Viewer(plugin);
|
||||
}
|
||||
|
||||
@@ -318,7 +326,10 @@ export class Viewer {
|
||||
source: {
|
||||
name: 'alphafolddb' as const,
|
||||
params: {
|
||||
id: afdb,
|
||||
provider: {
|
||||
id: afdb,
|
||||
encoding: 'bcif'
|
||||
},
|
||||
options: {
|
||||
...params.source.params.options,
|
||||
representation: 'preset-structure-representation-ma-quality-assessment-plddt'
|
||||
@@ -500,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);
|
||||
@@ -519,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
|
||||
}
|
||||
@@ -608,4 +619,9 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
|
||||
export const PluginExtensions = {
|
||||
wwPDBStructConn: wwPDBStructConnExtensionFunctions,
|
||||
mvs: { MVSData, loadMVS },
|
||||
modelArchive: {
|
||||
qualityAssessment: {
|
||||
config: MAQualityAssessmentConfig
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
|
||||
var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
|
||||
var powerPreference = getParam('power-preference', '[^&]+').trim().toLowerCase();
|
||||
var illumination = getParam('illumination', '[^&]+').trim() === '1';
|
||||
var resolutionMode = getParam('resolution-mode', '[^&]+').trim().toLowerCase();
|
||||
|
||||
// console.log('Available extensions: ', Object.keys(molstar.ExtensionMap));
|
||||
|
||||
@@ -83,6 +85,8 @@
|
||||
preferWebgl1: preferWebgl1,
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
illumination: illumination,
|
||||
resolutionMode: resolutionMode || 'auto'
|
||||
}).then(viewer => {
|
||||
var snapshotId = getParam('snapshot-id', '[^&]+').trim();
|
||||
if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
|
||||
|
||||
@@ -234,13 +234,19 @@ const FORCE_INT_FIELDS = [
|
||||
'_atom_site.id',
|
||||
'_atom_site.auth_seq_id',
|
||||
'_atom_site_anisotrop.id',
|
||||
'_atom_site_anisotrop.pdbx_auth_seq_id',
|
||||
'_pdbx_struct_mod_residue.auth_seq_id',
|
||||
'_pdbx_unobs_or_zero_occ_residues.auth_seq_id',
|
||||
'_struct_conf.beg_auth_seq_id',
|
||||
'_struct_conf.end_auth_seq_id',
|
||||
'_struct_conn.ptnr1_auth_seq_id',
|
||||
'_struct_conn.ptnr2_auth_seq_id',
|
||||
'_struct_sheet_range.beg_auth_seq_id',
|
||||
'_struct_sheet_range.end_auth_seq_id',
|
||||
'_struct_site.pdbx_auth_seq_id',
|
||||
'_struct_site_gen.auth_seq_id',
|
||||
'_struct_mon_prot_cis.auth_seq_id',
|
||||
'_struct_mon_prot_cis.pdbx_auth_seq_id_2',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,8 +13,8 @@ import { UniqueArray } from '../../mol-data/generic';
|
||||
|
||||
const LIPIDS_DIR = path.resolve(__dirname, '../../../../build/lipids/');
|
||||
|
||||
const MARTINI_LIPIDS_PATH = path.resolve(LIPIDS_DIR, 'martini_lipids.itp');
|
||||
const MARTINI_LIPIDS_URL = 'http://www.cgmartini.nl/images/parameters/lipids/Collections/martini_v2.0_lipids_all_201506.itp';
|
||||
const MARTINI_LIPIDS_PATH = path.resolve(LIPIDS_DIR, 'martini_lipids_v3.itp');
|
||||
const MARTINI_LIPIDS_URL = 'https://cgmartini-library.s3.ca-central-1.amazonaws.com/1_Downloads/ff_parameters/martini3/martini_v3.0.0_phospholipids_v1.itp';
|
||||
|
||||
async function ensureAvailable(path: string, url: string) {
|
||||
if (FORCE_DOWNLOAD || !fs.existsSync(path)) {
|
||||
@@ -32,6 +32,7 @@ async function ensureAvailable(path: string, url: string) {
|
||||
async function ensureLipidsAvailable() { await ensureAvailable(MARTINI_LIPIDS_PATH, MARTINI_LIPIDS_URL); }
|
||||
|
||||
const extraLipids = ['DMPC'];
|
||||
const v2lipids = ['DAPC', 'DBPC', 'DFPC', 'DGPC', 'DIPC', 'DLPC', 'DNPC', 'DOPC', 'DPPC', 'DRPC', 'DTPC', 'DVPC', 'DXPC', 'DYPC', 'LPPC', 'PAPC', 'PEPC', 'PGPC', 'PIPC', 'POPC', 'PRPC', 'PUPC', 'DAPE', 'DBPE', 'DFPE', 'DGPE', 'DIPE', 'DLPE', 'DNPE', 'DOPE', 'DPPE', 'DRPE', 'DTPE', 'DUPE', 'DVPE', 'DXPE', 'DYPE', 'LPPE', 'PAPE', 'PGPE', 'PIPE', 'POPE', 'PQPE', 'PRPE', 'PUPE', 'DAPS', 'DBPS', 'DFPS', 'DGPS', 'DIPS', 'DLPS', 'DNPS', 'DOPS', 'DPPS', 'DRPS', 'DTPS', 'DUPS', 'DVPS', 'DXPS', 'DYPS', 'LPPS', 'PAPS', 'PGPS', 'PIPS', 'POPS', 'PQPS', 'PRPS', 'PUPS', 'DAPG', 'DBPG', 'DFPG', 'DGPG', 'DIPG', 'DLPG', 'DNPG', 'DOPG', 'DPPG', 'DRPG', 'DTPG', 'DVPG', 'DXPG', 'DYPG', 'LPPG', 'PAPG', 'PGPG', 'PIPG', 'POPG', 'PRPG', 'DAPA', 'DBPA', 'DFPA', 'DGPA', 'DIPA', 'DLPA', 'DNPA', 'DOPA', 'DPPA', 'DRPA', 'DTPA', 'DVPA', 'DXPA', 'DYPA', 'LPPA', 'PAPA', 'PGPA', 'PIPA', 'POPA', 'PRPA', 'PUPA', 'DPP', 'DPPI', 'PAPI', 'PIPI', 'POP', 'POPI', 'PUPI', 'PVP', 'PVPI', 'PADG', 'PIDG', 'PODG', 'PUDG', 'PVDG', 'APC', 'CPC', 'IPC', 'LPC', 'OPC', 'PPC', 'TPC', 'UPC', 'VPC', 'BNSM', 'DBSM', 'DPSM', 'DXSM', 'PGSM', 'PNSM', 'POSM', 'PVSM', 'XNSM', 'DPCE', 'DXCE', 'PNCE', 'XNCE'];
|
||||
|
||||
async function run(out: string) {
|
||||
await ensureLipidsAvailable();
|
||||
@@ -50,11 +51,15 @@ async function run(out: string) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
for (const v of v2lipids) {
|
||||
UniqueArray.add(lipids, v, v);
|
||||
}
|
||||
|
||||
const lipidNames = JSON.stringify(lipids.array);
|
||||
|
||||
if (out) {
|
||||
const output = `/**
|
||||
* Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated lipid params file. Names extracted from Martini FF lipids itp.
|
||||
*
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
58
src/examples/alphafolddb-pae/index.html
Normal file
58
src/examples/alphafolddb-pae/index.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<title>Mol* 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>
|
||||
96
src/examples/alphafolddb-pae/index.tsx
Normal file
96
src/examples/alphafolddb-pae/index.tsx
Normal 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();
|
||||
97
src/examples/glb-export/index.ts
Normal file
97
src/examples/glb-export/index.ts
Normal 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();
|
||||
@@ -36,6 +36,7 @@ const Canvas3DPresets = {
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
transparentThreshold: 0.4,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
|
||||
@@ -182,7 +182,7 @@ export const CreateOrbitalRepresentation3D = PluginStateTransform.BuiltIn({
|
||||
return Task.create('Orbitals Representation 3D', async ctx => {
|
||||
const params = volumeParams(plugin, a, srcParams);
|
||||
|
||||
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset };
|
||||
const propertyCtx = { runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
|
||||
const provider = plugin.representation.volume.registry.get(params.type.name);
|
||||
if (provider.ensureCustomProperties) await provider.ensureCustomProperties.attach(propertyCtx, a.data);
|
||||
const props = params.type.params || {};
|
||||
|
||||
@@ -118,7 +118,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
|
||||
},
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Membrane Orientation', async ctx => {
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
|
||||
const repr = MembraneOrientationRepresentation({ webgl: plugin.canvas3d?.webgl, ...plugin.representation.structure.themes }, () => MembraneOrientationParams);
|
||||
await repr.createOrUpdate(params, a.data).runInContext(ctx);
|
||||
return new PluginStateObject.Shape.Representation3D({ repr, sourceData: a.data }, { label: 'Membrane Orientation' });
|
||||
@@ -126,7 +126,7 @@ const MembraneOrientation3D = PluginStateTransform.BuiltIn({
|
||||
},
|
||||
update({ a, b, newParams }, plugin: PluginContext) {
|
||||
return Task.create('Membrane Orientation', async ctx => {
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
await MembraneOrientationProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
|
||||
const props = { ...b.data.repr.props, ...newParams };
|
||||
await b.data.repr.createOrUpdate(props, a.data).runInContext(ctx);
|
||||
b.data.sourceData = a.data;
|
||||
@@ -155,7 +155,7 @@ export const MembraneOrientationPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
if (!MembraneOrientationProvider.get(structure).value) {
|
||||
await plugin.runTask(Task.create('Membrane Orientation', async runtime => {
|
||||
await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure);
|
||||
await MembraneOrientationProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -82,7 +82,7 @@ export const InitAssemblySymmetry3D = StateAction.build({
|
||||
params: (a, plugin: PluginContext) => getConfiguredDefaultParams(plugin)
|
||||
})(({ a, ref, state, params }, plugin: PluginContext) => Task.create('Init Assembly Symmetry', async ctx => {
|
||||
try {
|
||||
const propCtx = { runtime: ctx, assetManager: plugin.managers.asset };
|
||||
const propCtx = { runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, a.data, params);
|
||||
const assemblySymmetryData = AssemblySymmetryDataProvider.get(a.data).value;
|
||||
const symmetryIndex = assemblySymmetryData ? AssemblySymmetryData.firstNonC1(assemblySymmetryData) : -1;
|
||||
@@ -118,7 +118,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
|
||||
},
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Assembly Symmetry', async ctx => {
|
||||
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
|
||||
const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
|
||||
if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
|
||||
return StateObject.Null;
|
||||
@@ -131,7 +131,7 @@ const AssemblySymmetry3D = PluginStateTransform.BuiltIn({
|
||||
},
|
||||
update({ a, b, newParams }, plugin: PluginContext) {
|
||||
return Task.create('Assembly Symmetry', async ctx => {
|
||||
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset }, a.data);
|
||||
await AssemblySymmetryProvider.attach({ runtime: ctx, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, a.data);
|
||||
const assemblySymmetry = AssemblySymmetryProvider.get(a.data).value;
|
||||
if (!assemblySymmetry || assemblySymmetry.symbol === 'C1') {
|
||||
// this should NOT be StateTransformer.UpdateResult.Null
|
||||
@@ -176,7 +176,7 @@ export const AssemblySymmetryPreset = StructureRepresentationPresetProvider({
|
||||
|
||||
if (!AssemblySymmetryDataProvider.get(structure).value) {
|
||||
await plugin.runTask(Task.create('Assembly Symmetry', async runtime => {
|
||||
const propCtx = { runtime, assetManager: plugin.managers.asset };
|
||||
const propCtx = { runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext };
|
||||
const propProps = { serverType: params.serverType, serverUrl: params.serverUrl };
|
||||
await AssemblySymmetryDataProvider.attach(propCtx, structure, propProps);
|
||||
const assemblySymmetryData = AssemblySymmetryDataProvider.get(structure).value;
|
||||
|
||||
@@ -30,7 +30,7 @@ export const ConfalPyramidsPreset = StructureRepresentationPresetProvider({
|
||||
if (!structureCell || !model) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Confal Pyramids', async runtime => {
|
||||
await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
await ConfalPyramidsProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, model);
|
||||
}));
|
||||
|
||||
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
|
||||
|
||||
@@ -30,7 +30,7 @@ export const NtCTubePreset = StructureRepresentationPresetProvider({
|
||||
if (!structureCell || !model) return {};
|
||||
|
||||
await plugin.runTask(Task.create('NtC tube', async runtime => {
|
||||
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset }, model);
|
||||
await NtCTubeProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, model);
|
||||
}));
|
||||
|
||||
const { components, representations } = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
|
||||
|
||||
@@ -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: () => ({
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { QualityAssessment, QualityAssessmentProvider } from '../prop';
|
||||
import { Location } from '../../../../mol-model/location';
|
||||
import { Bond, StructureElement, Unit } from '../../../../mol-model/structure';
|
||||
import { Bond, Model, StructureElement, Unit } from '../../../../mol-model/structure';
|
||||
import { ColorTheme, LocationColor } from '../../../../mol-theme/color';
|
||||
import { ThemeDataContext } from '../../../../mol-theme/theme';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
@@ -91,7 +91,7 @@ export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfide
|
||||
factory: PLDDTConfidenceColorTheme,
|
||||
getParams: getPLDDTConfidenceColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT') || m.atomicConformation.B_iso_or_equiv.isDefined),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT') || (m.atomicConformation.B_iso_or_equiv.isDefined && !Model.isExperimental(m))),
|
||||
ensureCustomProperties: {
|
||||
attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => {
|
||||
if (data.structure) {
|
||||
|
||||
@@ -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>
|
||||
504
src/extensions/model-archive/quality-assessment/pairwise/ui.tsx
Normal file
504
src/extensions/model-archive/quality-assessment/pairwise/ui.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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 David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -76,7 +76,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
stoppedAnimation = false;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true, updateControls: true });
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
const image = await params.pass.getImageData(ctx, width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -106,6 +106,7 @@ export class MVSAnnotations {
|
||||
if (!file.ok) throw file.error;
|
||||
annots[spec.id] = await MVSAnnotation.fromSpec(ctx, spec, file.value);
|
||||
} catch (err) {
|
||||
ctx.errorContext?.add('mvs', `Failed to obtain annotation (${err}).\nAnnotation specification source params: ${JSON.stringify(spec.source.params)}`);
|
||||
console.error(`Failed to obtain annotation (${err}).\nAnnotation specification:`, spec);
|
||||
annots[spec.id] = MVSAnnotation.createEmpty(spec.schema);
|
||||
}
|
||||
|
||||
@@ -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 : [] });
|
||||
}));
|
||||
|
||||
|
||||
|
||||
662
src/extensions/mvs/components/primitives.ts
Normal file
662
src/extensions/mvs/components/primitives.ts
Normal 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);
|
||||
}
|
||||
53
src/extensions/mvs/components/surroundings.ts
Normal file
53
src/extensions/mvs/components/surroundings.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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: {} },
|
||||
});
|
||||
},
|
||||
};
|
||||
218
src/extensions/mvs/load-generic.ts
Normal file
218
src/extensions/mvs/load-generic.ts
Normal 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}`];
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -2,26 +2,33 @@
|
||||
* 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>
|
||||
* @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';
|
||||
|
||||
|
||||
@@ -29,57 +36,122 @@ 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 } = {}) {
|
||||
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;
|
||||
} finally {
|
||||
if (!options.doNotReportErrors) {
|
||||
for (const error of plugin.errorContext.get('mvs')) {
|
||||
plugin.log.warn(error);
|
||||
PluginCommands.Toast.Show(plugin, {
|
||||
title: 'Error',
|
||||
message: error,
|
||||
timeoutMs: 10000
|
||||
});
|
||||
}
|
||||
}
|
||||
plugin.errorContext.clear('mvs');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 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. */
|
||||
@@ -119,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,
|
||||
@@ -136,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;
|
||||
@@ -173,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;
|
||||
}
|
||||
@@ -184,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),
|
||||
});
|
||||
},
|
||||
@@ -213,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,
|
||||
];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
69
src/extensions/mvs/tree/generic/_spec/field-schema.spec.ts
Normal file
69
src/extensions/mvs/tree/generic/_spec/field-schema.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
@@ -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`
|
||||
});
|
||||
});
|
||||
|
||||
107
src/extensions/mvs/tree/generic/field-schema.ts
Normal file
107
src/extensions/mvs/tree/generic/field-schema.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
115
src/extensions/mvs/tree/mvs/mvs-tree-primitives.ts
Normal file
115
src/extensions/mvs/tree/mvs/mvs-tree-primitives.ts
Normal 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),
|
||||
},
|
||||
);
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ export const ValidationReportGeometryQualityPreset = StructureRepresentationPres
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = GeometryQualityColorThemeProvider.name as any;
|
||||
@@ -350,7 +350,7 @@ export const ValidationReportDensityFitPreset = StructureRepresentationPresetPro
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = DensityFitColorThemeProvider.name as any;
|
||||
@@ -374,7 +374,7 @@ export const ValidationReportRandomCoilIndexPreset = StructureRepresentationPres
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
await plugin.runTask(Task.create('Validation Report', async runtime => {
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset }, structure.models[0]);
|
||||
await ValidationReportProvider.attach({ runtime, assetManager: plugin.managers.asset, errorContext: plugin.errorContext }, structure.models[0]);
|
||||
}));
|
||||
|
||||
const colorTheme = RandomCoilIndexColorThemeProvider.name as any;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -28,8 +28,6 @@ interface ICamera {
|
||||
readonly fogNear: number,
|
||||
}
|
||||
|
||||
const tmpPos1 = Vec3();
|
||||
const tmpPos2 = Vec3();
|
||||
const tmpClip = Vec4();
|
||||
|
||||
class Camera implements ICamera {
|
||||
@@ -186,14 +184,11 @@ class Camera implements ICamera {
|
||||
|
||||
/** World space pixel size at given `point` */
|
||||
getPixelSize(point: Vec3) {
|
||||
// project -> unproject of `point` does not exactly return the same
|
||||
// to get a sufficiently accurate measure we unproject the original
|
||||
// clip position in addition to the one shifted by one pixel
|
||||
this.project(tmpClip, point);
|
||||
this.unproject(tmpPos1, tmpClip);
|
||||
tmpClip[0] += 1;
|
||||
this.unproject(tmpPos2, tmpClip);
|
||||
return Vec3.distance(tmpPos1, tmpPos2);
|
||||
const w = tmpClip[3];
|
||||
const rx = this.viewport.width;
|
||||
const P00 = this.projection[0];
|
||||
return (2 / w) / (rx * Math.abs(P00));
|
||||
}
|
||||
|
||||
constructor(state?: Partial<Camera.Snapshot>, viewport = Viewport.create(0, 0, 128, 128)) {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2025 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
* @author Herman Bergwerf <post@hbergwerf.nl>
|
||||
*/
|
||||
|
||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, Subject, Subscription, debounceTime, merge } from 'rxjs';
|
||||
import { now } from '../mol-util/now';
|
||||
import { Vec3, Vec2 } from '../mol-math/linear-algebra';
|
||||
import { InputObserver, ModifiersKeys, ButtonsType } from '../mol-util/input/input-observer';
|
||||
@@ -44,6 +45,8 @@ import { degToRad, radToDeg } from '../mol-math/misc';
|
||||
import { AssetManager } from '../mol-util/assets';
|
||||
import { deepClone } from '../mol-util/object';
|
||||
import { HiZParams, HiZPass } from './passes/hi-z';
|
||||
import { IlluminationParams } from './passes/illumination';
|
||||
import { isMobileBrowser } from '../mol-util/browser';
|
||||
|
||||
export const Canvas3DParams = {
|
||||
camera: PD.Group({
|
||||
@@ -87,11 +90,13 @@ export const Canvas3DParams = {
|
||||
sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
|
||||
transparentBackground: PD.Boolean(false),
|
||||
dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'extra pixels to around target to check in case target is empty' }),
|
||||
pickPadding: PD.Numeric(3, { min: 0, max: 10, step: 1 }, { description: 'Extra pixels to around target to check in case target is empty.' }),
|
||||
userInteractionReleaseMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time before the user is not considered interacting anymore.' }),
|
||||
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
marking: PD.Group(MarkingParams),
|
||||
illumination: PD.Group(IlluminationParams),
|
||||
hiZ: PD.Group(HiZParams),
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
@@ -119,7 +124,9 @@ interface Canvas3DContext {
|
||||
readonly contextRestored?: BehaviorSubject<now.Timestamp>
|
||||
readonly assetManager: AssetManager
|
||||
readonly changed?: BehaviorSubject<undefined>
|
||||
readonly pixelScale: number
|
||||
|
||||
syncPixelScale(): void
|
||||
setProps: (props?: Partial<Canvas3DContext.Props>) => void
|
||||
dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
|
||||
}
|
||||
@@ -134,11 +141,12 @@ namespace Canvas3DContext {
|
||||
preserveDrawingBuffer: true,
|
||||
preferWebGl1: false,
|
||||
|
||||
handleResize: () => {},
|
||||
handleResize: () => { },
|
||||
};
|
||||
export type Attribs = typeof DefaultAttribs
|
||||
|
||||
export const Params = {
|
||||
resolutionMode: PD.Select('auto', PD.arrayToOptions(['auto', 'scaled', 'native'] as const)),
|
||||
pixelScale: PD.Numeric(1, { min: 0.1, max: 2, step: 0.05 }),
|
||||
pickScale: PD.Numeric(0.25, { min: 0.1, max: 1, step: 0.05 }),
|
||||
transparency: PD.Select('wboit', [['blended', 'Blended'], ['wboit', 'Weighted, Blended'], ['dpoit', 'Depth Peeling']] as const),
|
||||
@@ -163,7 +171,21 @@ namespace Canvas3DContext {
|
||||
});
|
||||
if (gl === null) throw new Error('Could not create a WebGL rendering context');
|
||||
|
||||
const { pixelScale, pickScale, transparency } = p;
|
||||
const getPixelScale = () => {
|
||||
const scaled = (p.pixelScale / (typeof window !== 'undefined' ? (window?.devicePixelRatio || 1) : 1));
|
||||
if (p.resolutionMode === 'auto') {
|
||||
return isMobileBrowser() ? scaled : p.pixelScale;
|
||||
}
|
||||
return p.resolutionMode === 'native' ? p.pixelScale : scaled;
|
||||
};
|
||||
const syncPixelScale = () => {
|
||||
const pixelScale = getPixelScale();
|
||||
input.setPixelScale(pixelScale);
|
||||
webgl.setPixelScale(pixelScale);
|
||||
};
|
||||
|
||||
const { pickScale, transparency } = p;
|
||||
const pixelScale = getPixelScale();
|
||||
const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
|
||||
const webgl = createContext(gl, { pixelScale });
|
||||
const passes = new Passes(webgl, assetManager, { pickScale, transparency });
|
||||
@@ -224,16 +246,27 @@ namespace Canvas3DContext {
|
||||
contextRestored: webgl.contextRestored,
|
||||
assetManager,
|
||||
changed,
|
||||
get pixelScale() { return getPixelScale(); },
|
||||
|
||||
syncPixelScale,
|
||||
setProps: (props?: Partial<Props>) => {
|
||||
if (!props) return;
|
||||
|
||||
let hasChanged = false;
|
||||
let pixelScaleNeedsUpdate = false;
|
||||
|
||||
if (props.resolutionMode !== undefined && props.resolutionMode !== p.resolutionMode) {
|
||||
p.resolutionMode = props.resolutionMode;
|
||||
pixelScaleNeedsUpdate = true;
|
||||
}
|
||||
|
||||
if (props.pixelScale !== undefined && props.pixelScale !== p.pixelScale) {
|
||||
p.pixelScale = props.pixelScale;
|
||||
input.setPixelScale(props.pixelScale);
|
||||
webgl.setPixelScale(props.pixelScale);
|
||||
pixelScaleNeedsUpdate = true;
|
||||
}
|
||||
|
||||
if (pixelScaleNeedsUpdate) {
|
||||
syncPixelScale();
|
||||
a.handleResize();
|
||||
hasChanged = true;
|
||||
}
|
||||
@@ -346,6 +379,7 @@ namespace Canvas3D {
|
||||
const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
|
||||
const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
|
||||
const reprCount = new BehaviorSubject(0);
|
||||
const interactionEvent = new Subject<void>();
|
||||
|
||||
let startTime = now();
|
||||
const didDraw = new BehaviorSubject<now.Timestamp>(0 as now.Timestamp);
|
||||
@@ -358,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;
|
||||
@@ -443,6 +479,8 @@ namespace Canvas3D {
|
||||
scene.update(void 0, true);
|
||||
helper.handle.scene.update(void 0, true);
|
||||
helper.camera.scene.update(void 0, true);
|
||||
|
||||
interactionEvent.next();
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
@@ -460,6 +498,8 @@ namespace Canvas3D {
|
||||
return changed;
|
||||
}
|
||||
|
||||
let fenceSync: WebGLSync | null = null;
|
||||
|
||||
function render(force: boolean) {
|
||||
if (webgl.isContextLost) return false;
|
||||
|
||||
@@ -474,6 +514,14 @@ namespace Canvas3D {
|
||||
y > gl.drawingBufferHeight || y + height < 0
|
||||
) return false;
|
||||
|
||||
if (fenceSync !== null) {
|
||||
if (webgl.checkSyncStatus(fenceSync)) {
|
||||
fenceSync = null;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled);
|
||||
|
||||
let didRender = false;
|
||||
@@ -483,29 +531,55 @@ namespace Canvas3D {
|
||||
const shouldRender = force || cameraChanged || resized || forceNextRender;
|
||||
forceNextRender = false;
|
||||
|
||||
const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
|
||||
|
||||
if (shouldRender || multiSampleChanged || markingUpdated) {
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
cam = stereoCamera;
|
||||
if (passes.illumination.supported && p.illumination.enabled) {
|
||||
if (shouldRender || markingUpdated) {
|
||||
renderer.setOcclusionTest(null);
|
||||
passes.illumination.reset();
|
||||
}
|
||||
|
||||
if (isTimingMode) webgl.timer.mark('Canvas3D.render', true);
|
||||
const ctx = { renderer, camera: cam, scene, helper };
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
|
||||
multiSampleHelper.render(ctx, p, true, forceOn);
|
||||
} else {
|
||||
passes.draw.render(ctx, p, true);
|
||||
}
|
||||
hiZ.render(camera);
|
||||
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
|
||||
if (passes.illumination.shouldRender(p)
|
||||
&& ((!isActivelyInteracting && scene.count > 0) || passes.illumination.iteration === 0 || p.userInteractionReleaseMs === 0)
|
||||
) {
|
||||
if (isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true });
|
||||
const ctx = { renderer, camera, scene, helper };
|
||||
passes.illumination.render(ctx, p, true);
|
||||
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
|
||||
|
||||
// if only marking has updated, do not set the flag to dirty
|
||||
pickHelper.dirty = pickHelper.dirty || shouldRender;
|
||||
didRender = true;
|
||||
// if only marking has updated, do not set the flag to dirty
|
||||
pickHelper.dirty = pickHelper.dirty || shouldRender;
|
||||
didRender = true;
|
||||
}
|
||||
} else {
|
||||
const multiSampleChanged = multiSampleHelper.update(markingUpdated || shouldRender, p.multiSample);
|
||||
|
||||
if (shouldRender || multiSampleChanged || markingUpdated) {
|
||||
renderer.setOcclusionTest(hiZ.isOccluded);
|
||||
|
||||
let cam: Camera | StereoCamera = camera;
|
||||
if (p.camera.stereo.name === 'on') {
|
||||
stereoCamera.update();
|
||||
cam = stereoCamera;
|
||||
}
|
||||
|
||||
if (isTimingMode) webgl.timer.mark('Canvas3D.render', { captureStats: true });
|
||||
const ctx = { renderer, camera: cam, scene, helper };
|
||||
if (MultiSamplePass.isEnabled(p.multiSample)) {
|
||||
const forceOn = p.multiSample.reduceFlicker && !cameraChanged && markingUpdated && !controls.isAnimating;
|
||||
multiSampleHelper.render(ctx, p, true, forceOn);
|
||||
} else {
|
||||
passes.draw.render(ctx, p, true);
|
||||
}
|
||||
hiZ.render(camera);
|
||||
if (isTimingMode) webgl.timer.markEnd('Canvas3D.render');
|
||||
|
||||
// if only marking has updated, do not set the flag to dirty
|
||||
pickHelper.dirty = pickHelper.dirty || shouldRender;
|
||||
didRender = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didRender) {
|
||||
fenceSync = webgl.getFenceSync();
|
||||
}
|
||||
|
||||
return didRender;
|
||||
@@ -573,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) {
|
||||
@@ -774,11 +848,13 @@ namespace Canvas3D {
|
||||
transparentBackground: p.transparentBackground,
|
||||
dpoitIterations: p.dpoitIterations,
|
||||
pickPadding: p.pickPadding,
|
||||
userInteractionReleaseMs: p.userInteractionReleaseMs,
|
||||
viewport: p.viewport,
|
||||
|
||||
postprocessing: { ...p.postprocessing },
|
||||
marking: { ...p.marking },
|
||||
multiSample: { ...p.multiSample },
|
||||
illumination: { ...p.illumination },
|
||||
hiZ: { ...hiZ.props },
|
||||
renderer: { ...renderer.props },
|
||||
trackball: { ...controls.props },
|
||||
@@ -814,6 +890,37 @@ namespace Canvas3D {
|
||||
requestDraw();
|
||||
});
|
||||
|
||||
// Monitor user interactions
|
||||
let isDragging = false;
|
||||
let isActivelyInteracting = false;
|
||||
let interactionSubs = [
|
||||
input.drag.subscribe(() => {
|
||||
isDragging = true;
|
||||
}),
|
||||
input.interactionEnd.subscribe(() => {
|
||||
isDragging = false;
|
||||
}),
|
||||
merge(
|
||||
input.drag,
|
||||
input.pinch,
|
||||
input.wheel,
|
||||
input.interactionEnd,
|
||||
).subscribe(() => {
|
||||
interactionEvent.next();
|
||||
}),
|
||||
interactionEvent.subscribe(() => {
|
||||
isActivelyInteracting = true;
|
||||
}),
|
||||
interactionEvent.pipe(
|
||||
debounceTime(p.userInteractionReleaseMs)
|
||||
).subscribe(() => {
|
||||
isActivelyInteracting = isDragging;
|
||||
if (!isDragging && passes.illumination.supported && p.illumination.enabled) {
|
||||
requestDraw();
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
//
|
||||
|
||||
if (isDebugMode && canvas) {
|
||||
@@ -869,6 +976,10 @@ namespace Canvas3D {
|
||||
reprRenderObjects.clear();
|
||||
scene.clear();
|
||||
helper.debug.clear();
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
requestDraw();
|
||||
reprCount.next(reprRenderObjects.size);
|
||||
},
|
||||
@@ -962,6 +1073,7 @@ namespace Canvas3D {
|
||||
if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
|
||||
if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
|
||||
if (props.pickPadding !== undefined) p.pickPadding = props.pickPadding;
|
||||
if (props.userInteractionReleaseMs !== undefined) p.userInteractionReleaseMs = props.userInteractionReleaseMs;
|
||||
if (props.viewport !== undefined) {
|
||||
const doNotUpdate = p.viewport === props.viewport ||
|
||||
(p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
|
||||
@@ -981,6 +1093,7 @@ namespace Canvas3D {
|
||||
}
|
||||
if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
|
||||
if (props.marking) Object.assign(p.marking, props.marking);
|
||||
if (props.illumination) Object.assign(p.illumination, props.illumination);
|
||||
if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
|
||||
if (props.hiZ) hiZ.setProps(props.hiZ);
|
||||
if (props.renderer) renderer.setProps(props.renderer);
|
||||
@@ -1021,6 +1134,10 @@ namespace Canvas3D {
|
||||
dispose: () => {
|
||||
contextRestoredSub.unsubscribe();
|
||||
ctxChangedSub?.unsubscribe();
|
||||
|
||||
for (const s of interactionSubs) s.unsubscribe();
|
||||
interactionSubs = [];
|
||||
|
||||
cancelAnimationFrame(animationFrameHandle);
|
||||
|
||||
markBuffer = [];
|
||||
@@ -1031,6 +1148,10 @@ namespace Canvas3D {
|
||||
renderer.dispose();
|
||||
interactionHelper.dispose();
|
||||
hiZ.dispose();
|
||||
if (fenceSync !== null) {
|
||||
webgl.deleteSync(fenceSync);
|
||||
fenceSync = null;
|
||||
}
|
||||
|
||||
removeConsoleStatsProvider(consoleStats);
|
||||
}
|
||||
@@ -1039,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;
|
||||
@@ -1065,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Herman Bergwerf <post@hbergwerf.nl>
|
||||
*
|
||||
* This code has been modified from https://github.com/mrdoob/three.js/,
|
||||
* copyright (c) 2010-2018 three.js authors. MIT License
|
||||
@@ -10,7 +11,7 @@
|
||||
|
||||
import { Quat, Vec2, Vec3, EPSILON } from '../../mol-math/linear-algebra';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, GestureInput, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
|
||||
import { InputObserver, DragInput, WheelInput, PinchInput, ButtonsType, ModifiersKeys, KeyInput, MoveInput } from '../../mol-util/input/input-observer';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Camera } from '../camera';
|
||||
import { absMax, degToRad } from '../../mol-math/misc';
|
||||
@@ -67,7 +68,7 @@ export const TrackballControlsParams = {
|
||||
animate: PD.MappedStatic('off', {
|
||||
off: PD.EmptyGroup(),
|
||||
spin: PD.Group({
|
||||
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }),
|
||||
speed: PD.Numeric(1, { min: -20, max: 20, step: 1 }, { description: 'Rotation speed in radians per second' }),
|
||||
}, { description: 'Spin the 3D scene around the x-axis in view space' }),
|
||||
rock: PD.Group({
|
||||
speed: PD.Numeric(0.3, { min: -5, max: 5, step: 0.1 }),
|
||||
@@ -134,7 +135,6 @@ namespace TrackballControls {
|
||||
const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd);
|
||||
const wheelSub = input.wheel.subscribe(onWheel);
|
||||
const pinchSub = input.pinch.subscribe(onPinch);
|
||||
const gestureSub = input.gesture.subscribe(onGesture);
|
||||
const keyDownSub = input.keyDown.subscribe(onKeyDown);
|
||||
const keyUpSub = input.keyUp.subscribe(onKeyUp);
|
||||
const moveSub = input.move.subscribe(onMove);
|
||||
@@ -614,18 +614,27 @@ namespace TrackballControls {
|
||||
}
|
||||
}
|
||||
|
||||
function onPinch({ fractionDelta, buttons, modifiers }: PinchInput) {
|
||||
if (Binding.match(b.scrollZoom, buttons, modifiers)) {
|
||||
_isInteracting = true;
|
||||
function onPinch({ isStart, startX, startY, centerPageX, centerPageY, fractionDelta, buttons, modifiers }: PinchInput) {
|
||||
if (outsideViewport(startX, startY)) return;
|
||||
|
||||
const pan = Binding.match(b.dragPan, buttons, modifiers);
|
||||
const zoom = Binding.match(b.scrollZoom, buttons, modifiers);
|
||||
_isInteracting = pan || zoom;
|
||||
|
||||
if (pan) {
|
||||
getMouseOnScreen(centerPageX, centerPageY);
|
||||
if (isStart) {
|
||||
Vec2.copy(_panStart, mouseOnScreenVec2);
|
||||
Vec2.copy(_panEnd, _panStart);
|
||||
} else {
|
||||
Vec2.copy(_panEnd, mouseOnScreenVec2);
|
||||
}
|
||||
}
|
||||
if (zoom) {
|
||||
_zoomEnd[1] += p.gestureScaleFactor * fractionDelta;
|
||||
}
|
||||
}
|
||||
|
||||
function onGesture({ deltaScale }: GestureInput) {
|
||||
_isInteracting = true;
|
||||
_zoomEnd[1] += p.gestureScaleFactor * deltaScale;
|
||||
}
|
||||
|
||||
function onMove({ movementX, movementY }: MoveInput) {
|
||||
if (!input.pointerLock || movementX === undefined || movementY === undefined) return;
|
||||
|
||||
@@ -804,7 +813,6 @@ namespace TrackballControls {
|
||||
dragSub.unsubscribe();
|
||||
wheelSub.unsubscribe();
|
||||
pinchSub.unsubscribe();
|
||||
gestureSub.unsubscribe();
|
||||
interactionEndSub.unsubscribe();
|
||||
keyDownSub.unsubscribe();
|
||||
keyUpSub.unsubscribe();
|
||||
@@ -817,8 +825,8 @@ namespace TrackballControls {
|
||||
function spin(deltaT: number) {
|
||||
if (p.animate.name !== 'spin' || p.animate.params.speed === 0 || _isInteracting) return;
|
||||
|
||||
const frameSpeed = p.animate.params.speed / 1000;
|
||||
_spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
|
||||
const radPerMs = p.animate.params.speed / 1000;
|
||||
_spinSpeed[0] = deltaT * radPerMs / getRotateFactor();
|
||||
Vec2.add(_rotCurr, _rotPrev, _spinSpeed);
|
||||
}
|
||||
|
||||
|
||||
@@ -310,8 +310,29 @@ export class BackgroundPass {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isReady()) return;
|
||||
private readonly bgColor = Vec3();
|
||||
|
||||
clear(props: BackgroundProps, transparentBackground: boolean, backgroundColor: Color) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
if (this.isEnabled(props)) {
|
||||
if (transparentBackground) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
} else {
|
||||
Color.toVec3Normalized(this.bgColor, backgroundColor);
|
||||
state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1);
|
||||
}
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
state.enable(gl.BLEND);
|
||||
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
} else {
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
}
|
||||
|
||||
render(props: BackgroundProps) {
|
||||
if (!this.isEnabled(props) || !this.isReady()) return;
|
||||
|
||||
if (this.renderable.values.dVariant.ref.value === 'image') {
|
||||
this.updateImageScaling();
|
||||
|
||||
@@ -113,8 +113,9 @@ export class DofPass {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
|
||||
|
||||
if (this.renderable.values.dBlurSize.ref.value !== props.blurSize) {
|
||||
ValueCell.update(this.renderable.values.dBlurSize, props.blurSize);
|
||||
const blurSize = Math.round(props.blurSize * this.webgl.pixelRatio);
|
||||
if (this.renderable.values.dBlurSize.ref.value !== blurSize) {
|
||||
ValueCell.update(this.renderable.values.dBlurSize, blurSize);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,27 +167,27 @@ export class DrawPass {
|
||||
|
||||
// render opaque primitives
|
||||
if (scene.hasOpaque) {
|
||||
renderer.renderDpoitOpaque(scene.primitives, camera, null);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
renderer.renderOpaque(scene.primitives, camera);
|
||||
}
|
||||
|
||||
this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
|
||||
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
@@ -203,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,39 +225,49 @@ export class DrawPass {
|
||||
|
||||
// render opaque primitives
|
||||
if (scene.hasOpaque) {
|
||||
renderer.renderWboitOpaque(scene.primitives, camera, null);
|
||||
renderer.renderOpaque(scene.primitives, camera);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
this.wboit.bind();
|
||||
renderer.renderWboitTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
|
||||
// evaluate wboit
|
||||
target.bind();
|
||||
this.wboit.render();
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
this.postprocessing.render(camera, scene, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light, renderer.ambientColor);
|
||||
}
|
||||
|
||||
// render transparent primitives and volumes
|
||||
if (scene.opacityAverage < 1 || scene.volumes.renderables.length > 0) {
|
||||
// render volumes
|
||||
if (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.volumes, camera, this.depthTextureOpaque);
|
||||
|
||||
// evaluate wboit
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.target.bind();
|
||||
} else {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
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) {
|
||||
@@ -265,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) {
|
||||
@@ -274,26 +292,51 @@ 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();
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentDepthRequired(scene, postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps) || DofPass.isEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
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');
|
||||
@@ -313,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');
|
||||
@@ -322,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,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();
|
||||
@@ -434,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) {
|
||||
|
||||
675
src/mol-canvas3d/passes/illumination.ts
Normal file
675
src/mol-canvas3d/passes/illumination.ts
Normal file
@@ -0,0 +1,675 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { CopyRenderable, QuadSchema, QuadValues, createCopyRenderable } from '../../mol-gl/compute/util';
|
||||
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
import { Renderer, RendererProps } from '../../mol-gl/renderer';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { compose_frag } from '../../mol-gl/shader/illumination/compose.frag';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
|
||||
import { DrawPass } from './draw';
|
||||
import { MarkingPass, MarkingProps } from './marking';
|
||||
import { Helper } from '../helper/helper';
|
||||
import { DofPass } from './dof';
|
||||
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;
|
||||
dpoitIterations: number;
|
||||
illumination: IlluminationProps;
|
||||
renderer: RendererProps;
|
||||
postprocessing: PostprocessingProps;
|
||||
marking: MarkingProps;
|
||||
multiSample: MultiSampleProps;
|
||||
}
|
||||
|
||||
type RenderContext = {
|
||||
renderer: Renderer;
|
||||
camera: Camera;
|
||||
scene: Scene;
|
||||
helper: Helper;
|
||||
}
|
||||
|
||||
export const IlluminationParams = {
|
||||
enabled: PD.Boolean(false),
|
||||
maxIterations: PD.Numeric(5, { min: 0, max: 16, step: 1 }, { description: 'Maximum number of tracing iterations. Final iteration count is 2^x.' }),
|
||||
denoise: PD.Boolean(true),
|
||||
denoiseThreshold: PD.Interval([0.15, 1], { min: 0, max: 4, step: 0.01 }, { description: 'Threshold for denoising. Automatically adjusted within given interval based on current iteration.' }),
|
||||
ignoreOutline: PD.Boolean(true, { description: 'Ignore outline in illumination pass where it is generally not needed for visual clarity. Useful when illumination is often toggled on/off.' }),
|
||||
...TracingParams,
|
||||
};
|
||||
export type IlluminationProps = PD.Values<typeof IlluminationParams>
|
||||
|
||||
export class IlluminationPass {
|
||||
private readonly tracing: TracingPass;
|
||||
|
||||
private readonly transparentTarget: RenderTarget;
|
||||
private readonly outputTarget: RenderTarget;
|
||||
|
||||
readonly packedDepth: boolean;
|
||||
|
||||
private readonly copyRenderable: CopyRenderable;
|
||||
private readonly composeRenderable: ComposeRenderable;
|
||||
|
||||
private multiSampleComposeTarget: RenderTarget;
|
||||
private multiSampleHoldTarget: RenderTarget;
|
||||
private multiSampleAccumulateTarget: RenderTarget;
|
||||
private multiSampleCompose: MultiSampleComposeRenderable;
|
||||
|
||||
private _iteration = 0;
|
||||
get iteration() { return this._iteration; }
|
||||
|
||||
private _colorTarget: RenderTarget;
|
||||
get colorTarget() { return this._colorTarget; }
|
||||
|
||||
private _supported = false;
|
||||
get supported() {
|
||||
return this._supported;
|
||||
}
|
||||
|
||||
getMaxIterations(props: Props) {
|
||||
return Math.pow(2, props.illumination.maxIterations);
|
||||
}
|
||||
|
||||
static isSupported(webgl: WebGLContext) {
|
||||
const { drawBuffers, textureFloat, colorBufferFloat, depthTexture } = webgl.extensions;
|
||||
if (!textureFloat || !colorBufferFloat || !depthTexture || !drawBuffers) {
|
||||
if (isDebugMode) {
|
||||
const missing: string[] = [];
|
||||
if (!textureFloat) missing.push('textureFloat');
|
||||
if (!colorBufferFloat) missing.push('colorBufferFloat');
|
||||
if (!depthTexture) missing.push('depthTexture');
|
||||
if (!drawBuffers) missing.push('drawBuffers');
|
||||
console.log(`Missing "${missing.join('", "')}" extensions required for "illumination"`);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, private readonly drawPass: DrawPass) {
|
||||
if (!IlluminationPass.isSupported(webgl)) return;
|
||||
|
||||
const { colorTarget } = drawPass;
|
||||
const width = colorTarget.getWidth();
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
this.tracing = new TracingPass(webgl, this.drawPass);
|
||||
|
||||
this.transparentTarget = webgl.createRenderTarget(width, height, false, 'uint8', 'nearest');
|
||||
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.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);
|
||||
this.multiSampleAccumulateTarget = webgl.createRenderTarget(width, height, false, 'float32');
|
||||
this.multiSampleCompose = getMultiSampleComposeRenderable(webgl, this.outputTarget.texture);
|
||||
|
||||
this._supported = true;
|
||||
}
|
||||
|
||||
private renderInput(renderer: Renderer, camera: ICamera, scene: Scene, props: Props) {
|
||||
if (isTimingMode) this.webgl.timer.mark('IlluminationPass.renderInput');
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const hasTransparent = scene.opacityAverage < 1;
|
||||
const hasMarking = markingEnabled && scene.markerAverage > 0;
|
||||
|
||||
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.drawPass.depthTextureOpaque);
|
||||
|
||||
if (scene.volumes.renderables.length > 0) {
|
||||
renderer.renderWboitTransparent(scene.volumes, camera, this.drawPass.depthTextureOpaque);
|
||||
}
|
||||
|
||||
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.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.drawPass.depthTextureOpaque, dpoitTextures);
|
||||
|
||||
this.transparentTarget.bind();
|
||||
this.drawPass.dpoit.renderBlendBack();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
|
||||
}
|
||||
|
||||
// evaluate dpoit
|
||||
this.transparentTarget.bind();
|
||||
this.drawPass.dpoit.render();
|
||||
|
||||
if (scene.volumes.renderables.length > 0) {
|
||||
renderer.renderVolume(scene.volumes, camera, this.drawPass.depthTextureOpaque);
|
||||
}
|
||||
} else {
|
||||
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.renderVolume(scene.volumes, camera, this.drawPass.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
if (outlineEnabled || dofEnabled || ssaoEnabled) {
|
||||
this.drawPass.depthTargetTransparent.bind();
|
||||
if (scene.opacityAverage < 1) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
if (hasMarking) {
|
||||
const markingDepthTest = props.marking.ghostEdgeStrength < 1;
|
||||
if (markingDepthTest && scene.markerAverage !== 1) {
|
||||
this.drawPass.marking.depthTarget.bind();
|
||||
renderer.clear(false, true);
|
||||
renderer.renderMarkingDepth(scene.primitives, camera);
|
||||
}
|
||||
|
||||
this.drawPass.marking.maskTarget.bind();
|
||||
renderer.clear(false, true);
|
||||
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.transparentTarget);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
this.tracing.composeTarget.bind();
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
if (isTimingMode) this.webgl.timer.markEnd('IlluminationPass.renderInput');
|
||||
}
|
||||
|
||||
shouldRender(props: Props) {
|
||||
return this._supported && props.illumination.enabled && this._iteration < this.getMaxIterations(props);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
if (!this._supported) return;
|
||||
|
||||
const w = this.outputTarget.getWidth();
|
||||
const h = this.outputTarget.getHeight();
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.tracing.setSize(width, height);
|
||||
|
||||
this.transparentTarget.setSize(width, height);
|
||||
this.outputTarget.setSize(width, height);
|
||||
|
||||
ValueCell.update(this.copyRenderable.values.uTexSize, Vec2.set(this.copyRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.composeRenderable.values.uTexSize, Vec2.set(this.composeRenderable.values.uTexSize.ref.value, width, height));
|
||||
|
||||
this.multiSampleComposeTarget.setSize(width, height);
|
||||
this.multiSampleHoldTarget.setSize(width, height);
|
||||
this.multiSampleAccumulateTarget.setSize(width, height);
|
||||
ValueCell.update(this.multiSampleCompose.values.uTexSize, Vec2.set(this.multiSampleCompose.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
|
||||
this.drawPass.setSize(width, height);
|
||||
}
|
||||
|
||||
reset(clearAdjustedProps = false) {
|
||||
if (!this._supported) return;
|
||||
|
||||
this.tracing.reset(clearAdjustedProps);
|
||||
this._iteration = 0;
|
||||
this.prevSampleIndex = -1;
|
||||
}
|
||||
|
||||
private renderInternal(ctx: RenderContext, props: Props, toDrawingBuffer: boolean, forceRenderInput: boolean) {
|
||||
if (!this.shouldRender(props)) return;
|
||||
|
||||
if (isTimingMode) {
|
||||
this.webgl.timer.mark('IlluminationPass.render', {
|
||||
note: `iteration ${this._iteration + 1} of ${this.getMaxIterations(props)}`
|
||||
});
|
||||
}
|
||||
this.tracing.render(ctx, props.transparentBackground, props.illumination, this._iteration, forceRenderInput);
|
||||
|
||||
const { renderer, camera, scene, helper } = ctx;
|
||||
const { gl, state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
|
||||
if (this._iteration === 0 || forceRenderInput) {
|
||||
// render color & depth
|
||||
renderer.setTransparentBackground(props.transparentBackground);
|
||||
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
|
||||
renderer.setPixelRatio(this.webgl.pixelRatio);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
this.renderInput(renderer, camera, scene, props);
|
||||
}
|
||||
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.disable(gl.CULL_FACE);
|
||||
state.depthMask(false);
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
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;
|
||||
|
||||
if (this.composeRenderable.values.dOutlineEnable.ref.value !== outlinesEnabled) {
|
||||
needsUpdateCompose = true;
|
||||
ValueCell.update(this.composeRenderable.values.dOutlineEnable, outlinesEnabled);
|
||||
}
|
||||
|
||||
if (props.postprocessing.outline.name === 'on') {
|
||||
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));
|
||||
|
||||
if (this.composeRenderable.values.dOutlineScale.ref.value !== outlineScale) {
|
||||
needsUpdateCompose = true;
|
||||
ValueCell.update(this.composeRenderable.values.dOutlineScale, outlineScale);
|
||||
}
|
||||
if (this.composeRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
|
||||
needsUpdateCompose = true;
|
||||
ValueCell.update(this.composeRenderable.values.dTransparentOutline, transparentOutline);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uFogNear, camera.fogNear);
|
||||
ValueCell.update(this.composeRenderable.values.uFogColor, Color.toVec3Normalized(this.composeRenderable.values.uFogColor.ref.value, renderer.props.backgroundColor));
|
||||
if (this.composeRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
ValueCell.update(this.composeRenderable.values.dOrthographic, orthographic);
|
||||
needsUpdateCompose = true;
|
||||
}
|
||||
|
||||
// background
|
||||
|
||||
const _toDrawingBuffer = toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing) && props.postprocessing.dof.name === 'off';
|
||||
if (_toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.tracing.composeTarget.bind();
|
||||
}
|
||||
this._colorTarget = this.tracing.composeTarget;
|
||||
|
||||
this.drawPass.postprocessing.background.update(camera, props.postprocessing.background);
|
||||
this.drawPass.postprocessing.background.clear(props.postprocessing.background, props.transparentBackground, renderer.props.backgroundColor);
|
||||
this.drawPass.postprocessing.background.render(props.postprocessing.background);
|
||||
|
||||
// compose
|
||||
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uTransparentBackground, props.transparentBackground || this.drawPass.postprocessing.background.isEnabled(props.postprocessing.background));
|
||||
if (this.composeRenderable.values.dDenoise.ref.value !== props.illumination.denoise) {
|
||||
ValueCell.update(this.composeRenderable.values.dDenoise, props.illumination.denoise);
|
||||
needsUpdateCompose = true;
|
||||
}
|
||||
const denoiseThreshold = props.multiSample.mode === 'on'
|
||||
? props.illumination.denoiseThreshold[0]
|
||||
: lerp(props.illumination.denoiseThreshold[1], props.illumination.denoiseThreshold[0], clamp(this.iteration / (this.getMaxIterations(props) / 2), 0, 1));
|
||||
ValueCell.updateIfChanged(this.composeRenderable.values.uDenoiseThreshold, denoiseThreshold);
|
||||
if (needsUpdateCompose) this.composeRenderable.update();
|
||||
this.composeRenderable.render();
|
||||
|
||||
//
|
||||
|
||||
renderer.setDrawingBufferSize(this.tracing.composeTarget.getWidth(), this.tracing.composeTarget.getHeight());
|
||||
renderer.setPixelRatio(this.webgl.pixelRatio);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
|
||||
if (helper.debug.isEnabled) {
|
||||
helper.debug.syncVisibility();
|
||||
renderer.renderBlended(helper.debug.scene, camera);
|
||||
}
|
||||
if (helper.handle.isEnabled) {
|
||||
renderer.renderBlended(helper.handle.scene, camera);
|
||||
}
|
||||
if (helper.camera.isEnabled) {
|
||||
helper.camera.update(camera);
|
||||
renderer.update(helper.camera.camera, helper.camera.scene);
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
let targetIsDrawingbuffer = false;
|
||||
let swapTarget = this.outputTarget;
|
||||
|
||||
if (AntialiasingPass.isEnabled(props.postprocessing)) {
|
||||
const _toDrawingBuffer = toDrawingBuffer && props.postprocessing.dof.name === 'off';
|
||||
this.drawPass.antialiasing.render(camera, this.tracing.composeTarget.texture, _toDrawingBuffer ? true : this.outputTarget, props.postprocessing);
|
||||
|
||||
if (_toDrawingBuffer) {
|
||||
targetIsDrawingbuffer = true;
|
||||
} else {
|
||||
this._colorTarget = this.outputTarget;
|
||||
swapTarget = this.tracing.composeTarget;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.postprocessing.bloom.name === 'on') {
|
||||
const _toDrawingBuffer = (toDrawingBuffer && props.postprocessing.dof.name === 'off') || targetIsDrawingbuffer;
|
||||
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.drawPass.depthTextureOpaque, this.drawPass.depthTargetTransparent.texture, props.postprocessing.dof.params, scene.boundingSphereVisible);
|
||||
this.drawPass.dof.render(camera.viewport, _toDrawingBuffer ? undefined : swapTarget);
|
||||
|
||||
if (!_toDrawingBuffer) {
|
||||
this._colorTarget = swapTarget;
|
||||
}
|
||||
}
|
||||
|
||||
this._iteration += 1;
|
||||
if (isTimingMode) this.webgl.timer.markEnd('IlluminationPass.render');
|
||||
|
||||
this.webgl.gl.flush();
|
||||
}
|
||||
|
||||
private prevSampleIndex = -1;
|
||||
|
||||
private renderMultiSample(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
|
||||
const { camera } = ctx;
|
||||
const { multiSampleCompose, multiSampleComposeTarget, multiSampleHoldTarget, webgl } = this;
|
||||
const { gl, state } = webgl;
|
||||
|
||||
// based on the Multisample Anti-Aliasing Render Pass
|
||||
// contributed to three.js by bhouston / http://clara.io/
|
||||
//
|
||||
// This manual approach to MSAA re-renders the scene once for
|
||||
// each sample with camera jitter and accumulates the results.
|
||||
const offsetList = JitterVectors[Math.max(0, Math.min(props.multiSample.sampleLevel, 5))];
|
||||
|
||||
const maxIterations = this.getMaxIterations(props);
|
||||
const iteration = Math.min(this._iteration, maxIterations);
|
||||
|
||||
const sampleIndex = Math.floor((iteration / maxIterations) * offsetList.length);
|
||||
|
||||
if (isTimingMode) {
|
||||
webgl.timer.mark('IlluminationPass.renderMultiSample', {
|
||||
note: `sampleIndex ${sampleIndex + 1} of ${offsetList.length}`
|
||||
});
|
||||
}
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
const sampleWeight = 1.0 / maxIterations;
|
||||
|
||||
if (iteration === 0) {
|
||||
this.renderInternal(ctx, props, false, true);
|
||||
ValueCell.update(multiSampleCompose.values.uWeight, 1.0);
|
||||
ValueCell.update(multiSampleCompose.values.tColor, this._colorTarget.texture);
|
||||
multiSampleCompose.update();
|
||||
|
||||
multiSampleHoldTarget.bind();
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
multiSampleCompose.render();
|
||||
} else {
|
||||
camera.viewOffset.enabled = true;
|
||||
ValueCell.update(multiSampleCompose.values.tColor, this._colorTarget.texture);
|
||||
ValueCell.update(multiSampleCompose.values.uWeight, sampleWeight);
|
||||
multiSampleCompose.update();
|
||||
|
||||
// render the scene multiple times, each slightly jitter offset
|
||||
// from the last and accumulate the results.
|
||||
const offset = offsetList[sampleIndex];
|
||||
Camera.setViewOffset(camera.viewOffset, width, height, offset[0], offset[1], width, height);
|
||||
camera.update();
|
||||
|
||||
// render scene
|
||||
this.renderInternal(ctx, props, false, this.prevSampleIndex !== sampleIndex);
|
||||
|
||||
// compose rendered scene with compose target
|
||||
multiSampleComposeTarget.bind();
|
||||
state.enable(gl.BLEND);
|
||||
state.blendEquationSeparate(gl.FUNC_ADD, gl.FUNC_ADD);
|
||||
state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
if (iteration === 1) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
multiSampleCompose.render();
|
||||
}
|
||||
|
||||
this.prevSampleIndex = sampleIndex;
|
||||
|
||||
if (toDrawingBuffer) {
|
||||
this.webgl.unbindFramebuffer();
|
||||
} else {
|
||||
this.multiSampleAccumulateTarget.bind();
|
||||
}
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
const accumulationWeight = iteration * sampleWeight;
|
||||
if (accumulationWeight > 0) {
|
||||
ValueCell.update(multiSampleCompose.values.uWeight, 1.0);
|
||||
ValueCell.update(multiSampleCompose.values.tColor, multiSampleComposeTarget.texture);
|
||||
multiSampleCompose.update();
|
||||
state.disable(gl.BLEND);
|
||||
multiSampleCompose.render();
|
||||
}
|
||||
if (accumulationWeight < 1.0) {
|
||||
ValueCell.update(multiSampleCompose.values.uWeight, 1.0 - accumulationWeight);
|
||||
ValueCell.update(multiSampleCompose.values.tColor, multiSampleHoldTarget.texture);
|
||||
multiSampleCompose.update();
|
||||
if (accumulationWeight === 0) state.disable(gl.BLEND);
|
||||
else state.enable(gl.BLEND);
|
||||
multiSampleCompose.render();
|
||||
}
|
||||
|
||||
if (!toDrawingBuffer) {
|
||||
state.disable(gl.BLEND);
|
||||
this.colorTarget.bind();
|
||||
if (this.copyRenderable.values.tColor.ref.value !== this.multiSampleAccumulateTarget.texture) {
|
||||
ValueCell.update(this.copyRenderable.values.tColor, this.multiSampleAccumulateTarget.texture);
|
||||
this.copyRenderable.update();
|
||||
}
|
||||
this.copyRenderable.render();
|
||||
}
|
||||
|
||||
camera.viewOffset.enabled = false;
|
||||
camera.update();
|
||||
if (isTimingMode) webgl.timer.markEnd('IlluminationPass.renderMultiSample');
|
||||
}
|
||||
|
||||
render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
|
||||
if (!this._supported) return;
|
||||
|
||||
if (props.multiSample.mode === 'on') {
|
||||
this.renderMultiSample(ctx, props, toDrawingBuffer);
|
||||
} else {
|
||||
this.renderInternal(ctx, props, toDrawingBuffer, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const ComposeSchema = {
|
||||
...QuadSchema,
|
||||
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'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dDenoise: DefineSpec('boolean'),
|
||||
uDenoiseThreshold: UniformSpec('f'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uFogNear: UniformSpec('f'),
|
||||
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'),
|
||||
};
|
||||
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, 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),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
|
||||
dDenoise: ValueCell.create(true),
|
||||
uDenoiseThreshold: ValueCell.create(0.1),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(10000),
|
||||
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),
|
||||
};
|
||||
|
||||
const schema = { ...ComposeSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', ComposeShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const MultiSampleComposeSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
uWeight: UniformSpec('f'),
|
||||
};
|
||||
const MultiSampleComposeShaderCode = ShaderCode('compose', quad_vert, multiSample_compose_frag);
|
||||
type MultiSampleComposeRenderable = ComputeRenderable<Values<typeof MultiSampleComposeSchema>>
|
||||
|
||||
function getMultiSampleComposeRenderable(ctx: WebGLContext, colorTexture: Texture): MultiSampleComposeRenderable {
|
||||
const values: Values<typeof MultiSampleComposeSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
uWeight: ValueCell.create(1.0),
|
||||
};
|
||||
|
||||
const schema = { ...MultiSampleComposeSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', MultiSampleComposeShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* 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>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { Renderer } from '../../mol-gl/renderer';
|
||||
import { Renderer, RendererParams } from '../../mol-gl/renderer';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { DrawPass } from './draw';
|
||||
@@ -19,6 +19,10 @@ import { Helper } from '../helper/helper';
|
||||
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
|
||||
import { MarkingParams } from './marking';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { IlluminationParams, IlluminationPass } from './illumination';
|
||||
import { RuntimeContext } from '../../mol-task';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { printTimerResults } from '../../mol-gl/webgl/timer';
|
||||
|
||||
export const ImageParams = {
|
||||
transparentBackground: PD.Boolean(false),
|
||||
@@ -26,8 +30,10 @@ export const ImageParams = {
|
||||
multiSample: PD.Group(MultiSampleParams),
|
||||
postprocessing: PD.Group(PostprocessingParams),
|
||||
marking: PD.Group(MarkingParams),
|
||||
illumination: PD.Group(IlluminationParams),
|
||||
|
||||
cameraHelper: PD.Group(CameraHelperParams),
|
||||
renderer: PD.Group(RendererParams),
|
||||
};
|
||||
export type ImageProps = PD.Values<typeof ImageParams>
|
||||
|
||||
@@ -42,6 +48,7 @@ export class ImagePass {
|
||||
get colorTarget() { return this._colorTarget; }
|
||||
|
||||
private readonly drawPass: DrawPass;
|
||||
private readonly illuminationPass: IlluminationPass;
|
||||
private readonly multiSamplePass: MultiSamplePass;
|
||||
private readonly multiSampleHelper: MultiSampleHelper;
|
||||
private readonly helper: Helper;
|
||||
@@ -53,6 +60,7 @@ export class ImagePass {
|
||||
this.props = { ...PD.getDefaultValues(ImageParams), ...props };
|
||||
|
||||
this.drawPass = new DrawPass(webgl, assetManager, 128, 128, transparency);
|
||||
this.illuminationPass = new IlluminationPass(webgl, this.drawPass);
|
||||
this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
|
||||
this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
|
||||
|
||||
@@ -80,6 +88,7 @@ export class ImagePass {
|
||||
this._height = height;
|
||||
|
||||
this.drawPass.setSize(width, height);
|
||||
this.illuminationPass.setSize(width, height);
|
||||
this.multiSamplePass.syncSize();
|
||||
}
|
||||
|
||||
@@ -88,24 +97,59 @@ export class ImagePass {
|
||||
if (props.cameraHelper) this.helper.camera.setProps(props.cameraHelper);
|
||||
}
|
||||
|
||||
render() {
|
||||
async render(runtime: RuntimeContext) {
|
||||
Camera.copySnapshot(this._camera.state, this.camera.state);
|
||||
Viewport.set(this._camera.viewport, 0, 0, this._width, this._height);
|
||||
this._camera.update();
|
||||
|
||||
const ctx = { renderer: this.renderer, camera: this._camera, scene: this.scene, helper: this.helper };
|
||||
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
|
||||
this.multiSampleHelper.render(ctx, this.props, false);
|
||||
this._colorTarget = this.multiSamplePass.colorTarget;
|
||||
if (this.illuminationPass.supported && this.props.illumination.enabled) {
|
||||
await runtime.update({ message: 'Tracing...', current: 1, max: this.illuminationPass.getMaxIterations(this.props) });
|
||||
this.illuminationPass.reset(true);
|
||||
while (this.illuminationPass.shouldRender(this.props)) {
|
||||
if (isTimingMode) this.webgl.timer.mark('ImagePass.render', { captureStats: true });
|
||||
this.illuminationPass.render(ctx, this.props, false);
|
||||
if (isTimingMode) this.webgl.timer.markEnd('ImagePass.render');
|
||||
if (runtime.shouldUpdate) {
|
||||
await runtime.update({ current: this.illuminationPass.iteration });
|
||||
}
|
||||
await this.webgl.waitForGpuCommandsComplete();
|
||||
}
|
||||
this._colorTarget = this.illuminationPass.colorTarget;
|
||||
} else {
|
||||
this.drawPass.render(ctx, this.props, false);
|
||||
this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
|
||||
if (isTimingMode) this.webgl.timer.mark('ImagePass.render', { captureStats: true });
|
||||
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
|
||||
this.multiSampleHelper.render(ctx, this.props, false);
|
||||
this._colorTarget = this.multiSamplePass.colorTarget;
|
||||
} else {
|
||||
this.drawPass.render(ctx, this.props, false);
|
||||
this._colorTarget = this.drawPass.getColorTarget(this.props.postprocessing);
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('ImagePass.render');
|
||||
}
|
||||
|
||||
if (isTimingMode) {
|
||||
const timerResults = this.webgl.timer.resolve();
|
||||
if (timerResults) {
|
||||
for (const result of timerResults) {
|
||||
printTimerResults([result]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isTimingMode) {
|
||||
const timerResults = this.webgl.timer.resolve();
|
||||
if (timerResults) {
|
||||
for (const result of timerResults) {
|
||||
printTimerResults([result]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getImageData(width: number, height: number, viewport?: Viewport) {
|
||||
async getImageData(runtime: RuntimeContext, width: number, height: number, viewport?: Viewport) {
|
||||
this.setSize(width, height);
|
||||
this.render();
|
||||
await this.render(runtime);
|
||||
this.colorTarget.bind();
|
||||
|
||||
const w = viewport?.width ?? width, h = viewport?.height ?? height;
|
||||
|
||||
@@ -314,7 +314,7 @@ export class MultiSamplePass {
|
||||
}
|
||||
}
|
||||
|
||||
const JitterVectors = [
|
||||
export const JitterVectors = [
|
||||
[
|
||||
[0, 0]
|
||||
],
|
||||
|
||||
147
src/mol-canvas3d/passes/outline.ts
Normal file
147
src/mol-canvas3d/passes/outline.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const OutlineParams = {
|
||||
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
|
||||
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
|
||||
};
|
||||
|
||||
export type OutlineProps = PD.Values<typeof OutlineParams>
|
||||
|
||||
export class OutlinePass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.outline.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
private readonly renderable: OutlinesRenderable;
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, width: number, height: number, depthTextureTransparent: Texture, depthTextureOpaque: Texture) {
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
this.renderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, props: OutlineProps, depthTextureTransparent: Texture, depthTextureOpaque: Texture) {
|
||||
let needsUpdate = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const invProjection = this.renderable.values.uInvProjection.ref.value;
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const transparentOutline = props.includeTransparent ?? true;
|
||||
const outlineScale = Math.max(1, Math.round(props.scale * this.webgl.pixelRatio)) - 1;
|
||||
const outlineThreshold = 50 * props.threshold * this.webgl.pixelRatio;
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
if (this.renderable.values.dTransparentOutline.ref.value !== transparentOutline) {
|
||||
needsUpdate = true;
|
||||
ValueCell.update(this.renderable.values.dTransparentOutline, transparentOutline);
|
||||
}
|
||||
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdate = true;
|
||||
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.renderable.values.uOutlineThreshold, outlineThreshold);
|
||||
|
||||
if (this.renderable.values.tDepthTransparent.ref.value !== depthTextureTransparent) {
|
||||
needsUpdate = true;
|
||||
ValueCell.update(this.renderable.values.tDepthTransparent, depthTextureTransparent);
|
||||
}
|
||||
if (this.renderable.values.tDepthOpaque.ref.value !== depthTextureOpaque) {
|
||||
needsUpdate = true;
|
||||
ValueCell.update(this.renderable.values.tDepthOpaque, depthTextureOpaque);
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
}
|
||||
|
||||
return { transparentOutline, outlineScale };
|
||||
}
|
||||
|
||||
render() {
|
||||
if (isTimingMode) this.webgl.timer.mark('OutlinePass.render');
|
||||
this.target.bind();
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('OutlinePass.render');
|
||||
}
|
||||
}
|
||||
|
||||
export const OutlinesSchema = {
|
||||
...QuadSchema,
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
};
|
||||
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
|
||||
|
||||
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
|
||||
const width = depthTextureOpaque.getWidth();
|
||||
const height = depthTextureOpaque.getHeight();
|
||||
|
||||
const values: Values<typeof OutlinesSchema> = {
|
||||
...QuadValues,
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
|
||||
uOutlineThreshold: ValueCell.create(0.33),
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
};
|
||||
|
||||
const schema = { ...OutlinesSchema };
|
||||
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -9,17 +9,20 @@ import { PickPass } from './pick';
|
||||
import { MultiSamplePass } from './multi-sample';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { IlluminationPass } from './illumination';
|
||||
|
||||
export class Passes {
|
||||
readonly draw: DrawPass;
|
||||
readonly pick: PickPass;
|
||||
readonly multiSample: MultiSamplePass;
|
||||
readonly illumination: IlluminationPass;
|
||||
|
||||
constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, transparency: 'wboit' | 'dpoit' | 'blended' }> = {}) {
|
||||
const { gl } = webgl;
|
||||
this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.transparency || 'blended');
|
||||
this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
|
||||
this.multiSample = new MultiSamplePass(webgl, this.draw);
|
||||
this.illumination = new IlluminationPass(webgl, this.draw);
|
||||
}
|
||||
|
||||
setPickScale(pickScale: number) {
|
||||
@@ -38,5 +41,6 @@ export class Passes {
|
||||
this.draw.setSize(width, height);
|
||||
this.pick.syncSize();
|
||||
this.multiSample.syncSize();
|
||||
this.illumination.setSize(width, height);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ export class PickHelper {
|
||||
}
|
||||
|
||||
private render(camera: Camera | StereoCamera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', true);
|
||||
if (isTimingMode) this.webgl.timer.mark('PickHelper.render', { captureStats: true });
|
||||
const { pickX, pickY, pickWidth, pickHeight, halfPickWidth } = this;
|
||||
const { renderer, scene, helper } = this;
|
||||
|
||||
|
||||
@@ -4,27 +4,25 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { deepEqual, ValueCell } from '../../mol-util';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { DrawPass } from './draw';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
|
||||
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
|
||||
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
|
||||
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { FxaaParams, FxaaPass } from './fxaa';
|
||||
import { SmaaParams, SmaaPass } from './smaa';
|
||||
@@ -32,262 +30,21 @@ import { isTimingMode } from '../../mol-util/debug';
|
||||
import { BackgroundParams, BackgroundPass } from './background';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
|
||||
import { CasParams, CasPass } from './cas';
|
||||
import { DofParams } from './dof';
|
||||
import { DofPass, DofParams } from './dof';
|
||||
import { BloomParams } from './bloom';
|
||||
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
|
||||
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
|
||||
import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
|
||||
|
||||
export const OutlinesSchema = {
|
||||
...QuadSchema,
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
};
|
||||
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
|
||||
|
||||
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
|
||||
const width = depthTextureOpaque.getWidth();
|
||||
const height = depthTextureOpaque.getHeight();
|
||||
|
||||
const values: Values<typeof OutlinesSchema> = {
|
||||
...QuadValues,
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
|
||||
uOutlineThreshold: ValueCell.create(0.33),
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
};
|
||||
|
||||
const schema = { ...OutlinesSchema };
|
||||
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const ShadowsSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
|
||||
dSteps: DefineSpec('number'),
|
||||
uMaxDistance: UniformSpec('f'),
|
||||
uTolerance: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
uLightDirection: UniformSpec('v3[]'),
|
||||
uLightColor: UniformSpec('v3[]'),
|
||||
dLightCount: DefineSpec('number'),
|
||||
};
|
||||
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
|
||||
|
||||
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
|
||||
const width = depthTexture.getWidth();
|
||||
const height = depthTexture.getHeight();
|
||||
|
||||
const values: Values<typeof ShadowsSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
|
||||
dSteps: ValueCell.create(1),
|
||||
uMaxDistance: ValueCell.create(3.0),
|
||||
uTolerance: ValueCell.create(1.0),
|
||||
uBias: ValueCell.create(0.6),
|
||||
|
||||
uLightDirection: ValueCell.create([]),
|
||||
uLightColor: ValueCell.create([]),
|
||||
dLightCount: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...ShadowsSchema };
|
||||
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
|
||||
uSamples: UniformSpec('v3[]'),
|
||||
dNSamples: DefineSpec('number'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uRadius: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
dMultiScale: DefineSpec('boolean'),
|
||||
dLevels: DefineSpec('number'),
|
||||
uLevelRadius: UniformSpec('f[]'),
|
||||
uLevelBias: UniformSpec('f[]'),
|
||||
uNearThreshold: UniformSpec('f'),
|
||||
uFarThreshold: UniformSpec('f'),
|
||||
};
|
||||
|
||||
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
|
||||
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
|
||||
const values: Values<typeof SsaoSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
tDepthHalf: ValueCell.create(depthHalfTexture),
|
||||
tDepthQuarter: ValueCell.create(depthQuarterTexture),
|
||||
|
||||
uSamples: ValueCell.create(getSamples(32)),
|
||||
dNSamples: ValueCell.create(32),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
uRadius: ValueCell.create(Math.pow(2, 5)),
|
||||
uBias: ValueCell.create(0.8),
|
||||
|
||||
dMultiScale: ValueCell.create(false),
|
||||
dLevels: ValueCell.create(3),
|
||||
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
|
||||
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
|
||||
uNearThreshold: ValueCell.create(10.0),
|
||||
uFarThreshold: ValueCell.create(1500.0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoSchema };
|
||||
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoBlurSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uKernel: UniformSpec('f[]'),
|
||||
dOcclusionKernelSize: DefineSpec('number'),
|
||||
uBlurDepthBias: UniformSpec('f'),
|
||||
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
dOrthographic: DefineSpec('number'),
|
||||
};
|
||||
|
||||
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
|
||||
|
||||
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
|
||||
const values: Values<typeof SsaoBlurSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
|
||||
|
||||
uKernel: ValueCell.create(getBlurKernel(15)),
|
||||
dOcclusionKernelSize: ValueCell.create(15),
|
||||
uBlurDepthBias: ValueCell.create(0.5),
|
||||
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
dOrthographic: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoBlurSchema };
|
||||
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function getBlurKernel(kernelSize: number): number[] {
|
||||
const sigma = kernelSize / 3.0;
|
||||
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
|
||||
|
||||
const kernel = [];
|
||||
for (let x = 0; x < halfKernelSize; x++) {
|
||||
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
const RandomHemisphereVector: Vec3[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const v = Vec3();
|
||||
v[0] = Math.random() * 2.0 - 1.0;
|
||||
v[1] = Math.random() * 2.0 - 1.0;
|
||||
v[2] = Math.random();
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, Math.random());
|
||||
RandomHemisphereVector.push(v);
|
||||
}
|
||||
|
||||
function getSamples(nSamples: number): number[] {
|
||||
const samples = [];
|
||||
for (let i = 0; i < nSamples; i++) {
|
||||
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
|
||||
scale = 0.1 + scale * (1.0 - 0.1);
|
||||
|
||||
samples.push(RandomHemisphereVector[i][0] * scale);
|
||||
samples.push(RandomHemisphereVector[i][1] * scale);
|
||||
samples.push(RandomHemisphereVector[i][2] * scale);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
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'),
|
||||
@@ -305,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'),
|
||||
@@ -315,11 +75,14 @@ const PostprocessingSchema = {
|
||||
};
|
||||
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
|
||||
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, 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),
|
||||
@@ -337,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),
|
||||
@@ -355,49 +121,15 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
|
||||
|
||||
export const PostprocessingParams = {
|
||||
occlusion: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
multiScale: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
levels: PD.ObjectList({
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
|
||||
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
|
||||
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
|
||||
{ radius: 2, bias: 1 },
|
||||
{ radius: 5, bias: 1 },
|
||||
{ radius: 8, bias: 1 },
|
||||
{ radius: 11, bias: 1 },
|
||||
] }),
|
||||
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
|
||||
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true }),
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
|
||||
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
|
||||
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)),
|
||||
}),
|
||||
on: PD.Group(SsaoParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
|
||||
shadow: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
|
||||
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
|
||||
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
}),
|
||||
on: PD.Group(ShadowParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Simplistic shadows' }),
|
||||
outline: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
|
||||
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
|
||||
}),
|
||||
on: PD.Group(OutlineParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' }),
|
||||
dof: PD.MappedStatic('off', {
|
||||
@@ -422,384 +154,95 @@ export const PostprocessingParams = {
|
||||
|
||||
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
|
||||
|
||||
type Levels = {
|
||||
count: number
|
||||
radius: number[]
|
||||
bias: number[]
|
||||
}
|
||||
|
||||
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
|
||||
const count = props.length;
|
||||
const { radius, bias } = levels || {
|
||||
radius: (new Array(count * 3)).fill(0),
|
||||
bias: (new Array(count * 3)).fill(0),
|
||||
};
|
||||
props = props.slice().sort((a, b) => a.radius - b.radius);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
radius[i] = Math.pow(2, p.radius);
|
||||
bias[i] = p.bias;
|
||||
}
|
||||
return { count, radius, bias };
|
||||
}
|
||||
|
||||
export class PostprocessingPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
|
||||
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
|
||||
}
|
||||
|
||||
static 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 props.outline.name === 'on' && props.outline.params.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;
|
||||
|
||||
private readonly outlinesTarget: RenderTarget;
|
||||
private readonly outlinesRenderable: OutlinesRenderable;
|
||||
|
||||
private readonly shadowsTarget: RenderTarget;
|
||||
private readonly shadowsRenderable: ShadowsRenderable;
|
||||
|
||||
private readonly ssaoFramebuffer: Framebuffer;
|
||||
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
|
||||
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
|
||||
|
||||
private readonly downsampledDepthTarget: RenderTarget;
|
||||
private readonly downsampleDepthRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTarget: RenderTarget;
|
||||
private readonly depthHalfRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthQuarterTarget: RenderTarget;
|
||||
private readonly depthQuarterRenderable: CopyRenderable;
|
||||
|
||||
private readonly ssaoDepthTexture: Texture;
|
||||
private readonly ssaoDepthBlurProxyTexture: Texture;
|
||||
|
||||
private readonly ssaoRenderable: SsaoRenderable;
|
||||
private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
|
||||
private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
|
||||
private readonly renderable: PostprocessingRenderable;
|
||||
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
|
||||
}
|
||||
|
||||
private levels: { radius: number, bias: number }[];
|
||||
|
||||
private readonly bgColor = Vec3();
|
||||
readonly ssao: SsaoPass;
|
||||
readonly shadow: ShadowPass;
|
||||
readonly outline: OutlinePass;
|
||||
readonly background: BackgroundPass;
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) {
|
||||
const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
|
||||
constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, readonly drawPass: DrawPass) {
|
||||
const { colorTarget, transparentColorTarget, depthTextureOpaque, depthTextureTransparent, packedDepth } = drawPass;
|
||||
const width = colorTarget.getWidth();
|
||||
const height = colorTarget.getHeight();
|
||||
|
||||
this.nSamples = 1;
|
||||
this.blurKernelSize = 1;
|
||||
this.ssaoScale = this.calcSsaoScale(1);
|
||||
this.levels = [];
|
||||
|
||||
// needs to be linear for anti-aliasing pass
|
||||
this.target = webgl.createRenderTarget(width, height, false, 'uint8', 'linear');
|
||||
|
||||
this.outlinesTarget = webgl.createRenderTarget(width, height, false);
|
||||
this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent, true);
|
||||
this.ssao = new SsaoPass(webgl, width, height, packedDepth, depthTextureOpaque, depthTextureTransparent);
|
||||
this.shadow = new ShadowPass(webgl, width, height, depthTextureOpaque);
|
||||
this.outline = new OutlinePass(webgl, width, height, depthTextureTransparent, depthTextureOpaque);
|
||||
|
||||
this.shadowsTarget = webgl.createRenderTarget(width, height, false);
|
||||
this.shadowsRenderable = getShadowsRenderable(webgl, depthTextureOpaque);
|
||||
|
||||
this.ssaoFramebuffer = webgl.resources.framebuffer();
|
||||
this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
|
||||
this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
|
||||
const hw = Math.max(1, Math.floor(sw * 0.5));
|
||||
const hh = Math.max(1, Math.floor(sh * 0.5));
|
||||
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
|
||||
this.downsampledDepthTarget = drawPass.packedDepth
|
||||
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'nearest', 'rgba')
|
||||
: webgl.createRenderTarget(sw, sh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTextureOpaque);
|
||||
|
||||
const depthTexture = this.ssaoScale === 1 ? depthTextureOpaque : this.downsampledDepthTarget.texture;
|
||||
|
||||
this.depthHalfTarget = drawPass.packedDepth
|
||||
? webgl.createRenderTarget(hw, hh, false, 'uint8', 'nearest', 'rgba')
|
||||
: webgl.createRenderTarget(hw, hh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthHalfRenderable = createCopyRenderable(webgl, depthTexture);
|
||||
|
||||
this.depthQuarterTarget = drawPass.packedDepth
|
||||
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'nearest', 'rgba')
|
||||
: webgl.createRenderTarget(qw, qh, false, 'float32', 'nearest', webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
|
||||
|
||||
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.ssaoFramebuffer, 'color0');
|
||||
|
||||
this.ssaoDepthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.attachFramebuffer(this.ssaoBlurFirstPassFramebuffer, 'color0');
|
||||
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.ssaoBlurSecondPassFramebuffer, 'color0');
|
||||
|
||||
this.ssaoRenderable = getSsaoRenderable(webgl, depthTexture, this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
|
||||
this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
|
||||
this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.shadowsTarget.texture, this.outlinesTarget.texture, this.ssaoDepthTexture, true);
|
||||
this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, 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);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const ssaoScale = this.calcSsaoScale(1);
|
||||
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
this.outlinesTarget.setSize(width, height);
|
||||
this.shadowsTarget.setSize(width, height);
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
this.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.max(1, Math.floor(sw * 0.5));
|
||||
const hh = Math.max(1, Math.floor(sh * 0.5));
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
|
||||
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
|
||||
|
||||
this.depthHalfRenderable.update();
|
||||
this.ssaoRenderable.update();
|
||||
|
||||
this.background.setSize(width, height);
|
||||
}
|
||||
|
||||
this.ssao.setSize(width, height);
|
||||
this.shadow.setSize(width, height);
|
||||
this.outline.setSize(width, height);
|
||||
this.background.setSize(width, height);
|
||||
}
|
||||
|
||||
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
|
||||
let needsUpdateShadows = false;
|
||||
updateState(camera: ICamera, scene: Scene, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light, ambientColor: Vec3) {
|
||||
let needsUpdateMain = false;
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
let needsUpdateOutlines = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
const outlinesEnabled = props.outline.name === 'on';
|
||||
const shadowsEnabled = props.shadow.name === 'on';
|
||||
const occlusionEnabled = props.occlusion.name === 'on';
|
||||
const outlinesEnabled = OutlinePass.isEnabled(props);
|
||||
const shadowsEnabled = ShadowPass.isEnabled(props);
|
||||
const occlusionEnabled = SsaoPass.isEnabled(props);
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
if (props.occlusion.name === 'on') {
|
||||
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
const b = this.ssaoRenderable.values.uBounds;
|
||||
const s = this.ssaoScale;
|
||||
Vec4.set(b.ref.value,
|
||||
Math.floor(v.x * s) / (w * s),
|
||||
Math.floor(v.y * s) / (h * s),
|
||||
Math.ceil((v.x + v.width) * s) / (w * s),
|
||||
Math.ceil((v.y + v.height) * s) / (h * s)
|
||||
);
|
||||
ValueCell.update(b, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBlurDepthBias, props.occlusion.params.blurDepthBias);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBlurDepthBias, props.occlusion.params.blurDepthBias);
|
||||
|
||||
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
|
||||
if (occlusionEnabled) {
|
||||
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);
|
||||
}
|
||||
|
||||
if (this.nSamples !== props.occlusion.params.samples) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.nSamples = props.occlusion.params.samples;
|
||||
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
|
||||
}
|
||||
|
||||
const multiScale = props.occlusion.params.multiScale.name === 'on';
|
||||
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
|
||||
needsUpdateSsao = true;
|
||||
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
|
||||
}
|
||||
|
||||
if (props.occlusion.params.multiScale.name === 'on') {
|
||||
const mp = props.occlusion.params.multiScale.params;
|
||||
if (!deepEqual(this.levels, mp.levels)) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.levels = mp.levels;
|
||||
const levels = getLevels(mp.levels);
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
|
||||
|
||||
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
|
||||
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
|
||||
|
||||
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
this.blurKernelSize = props.occlusion.params.blurKernelSize;
|
||||
const kernel = getBlurKernel(this.blurKernelSize);
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
}
|
||||
|
||||
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
|
||||
if (this.ssaoScale !== ssaoScale) {
|
||||
needsUpdateSsao = true;
|
||||
needsUpdateDepthHalf = true;
|
||||
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
const sw = Math.floor(w * this.ssaoScale);
|
||||
const sh = Math.floor(h * this.ssaoScale);
|
||||
this.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.floor(sw * 0.5);
|
||||
const hh = Math.floor(sh * 0.5);
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.floor(sw * 0.25);
|
||||
const qh = Math.floor(sh * 0.25);
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
|
||||
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
}
|
||||
|
||||
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
|
||||
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, params.color));
|
||||
}
|
||||
|
||||
if (props.shadow.name === 'on') {
|
||||
ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
|
||||
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
|
||||
if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
|
||||
ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
|
||||
if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
if (shadowsEnabled) {
|
||||
this.shadow.update(camera, light, ambientColor, props.shadow.params as ShadowProps);
|
||||
}
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
const transparentOutline = props.outline.params.includeTransparent ?? true;
|
||||
const outlineScale = Math.max(1, Math.round(props.outline.params.scale * this.webgl.pixelRatio)) - 1;
|
||||
const outlineThreshold = 50 * props.outline.params.threshold * this.webgl.pixelRatio;
|
||||
if (outlinesEnabled) {
|
||||
const outlineProps = props.outline.params as OutlineProps;
|
||||
const { transparentOutline, outlineScale } = this.outline.update(camera, outlineProps, this.drawPass.depthTextureTransparent, this.drawPass.depthTextureOpaque);
|
||||
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uFar, camera.far);
|
||||
ValueCell.update(this.outlinesRenderable.values.uInvProjection, invProjection);
|
||||
if (this.outlinesRenderable.values.dTransparentOutline.ref.value !== transparentOutline) {
|
||||
needsUpdateOutlines = true;
|
||||
ValueCell.update(this.outlinesRenderable.values.dTransparentOutline, transparentOutline);
|
||||
}
|
||||
if (this.outlinesRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateOutlines = true;
|
||||
ValueCell.update(this.outlinesRenderable.values.dOrthographic, orthographic);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.outlinesRenderable.values.uOutlineThreshold, outlineThreshold);
|
||||
|
||||
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, props.outline.params.color));
|
||||
ValueCell.update(this.renderable.values.uOutlineColor, Color.toVec3Normalized(this.renderable.values.uOutlineColor.ref.value, outlineProps.color));
|
||||
|
||||
if (this.renderable.values.dOutlineScale.ref.value !== outlineScale) {
|
||||
needsUpdateMain = true;
|
||||
@@ -817,6 +260,7 @@ export class PostprocessingPass {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
|
||||
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
|
||||
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
|
||||
|
||||
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
|
||||
@@ -835,25 +279,10 @@ export class PostprocessingPass {
|
||||
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
if (needsUpdateOutlines) {
|
||||
this.outlinesRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateShadows) {
|
||||
this.shadowsRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsao) {
|
||||
this.ssaoRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsaoBlur) {
|
||||
this.ssaoBlurFirstPassRenderable.update();
|
||||
this.ssaoBlurSecondPassRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderable.update();
|
||||
const blendTransparency = scene.opacityAverage < 1;
|
||||
if (this.renderable.values.dBlendTransparency.ref.value !== blendTransparency) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dBlendTransparency, blendTransparency);
|
||||
}
|
||||
|
||||
if (needsUpdateMain) {
|
||||
@@ -880,64 +309,28 @@ export class PostprocessingPass {
|
||||
this.transparentBackground = value;
|
||||
}
|
||||
|
||||
render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
|
||||
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);
|
||||
this.updateState(camera, scene, transparentBackground, backgroundColor, props, light, ambientColor);
|
||||
|
||||
const { gl, state } = this.webgl;
|
||||
const { state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
|
||||
// don't render occlusion if offset is given,
|
||||
// which will reuse the existing occlusion
|
||||
if (props.occlusion.name === 'on' && this.occlusionOffset[0] === 0 && this.occlusionOffset[1] === 0) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.render');
|
||||
const sx = Math.floor(x * this.ssaoScale);
|
||||
const sy = Math.floor(y * this.ssaoScale);
|
||||
const sw = Math.ceil(width * this.ssaoScale);
|
||||
const sh = Math.ceil(height * this.ssaoScale);
|
||||
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
|
||||
if (this.ssaoScale < 1) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
|
||||
this.downsampledDepthTarget.bind();
|
||||
this.downsampleDepthRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.half');
|
||||
this.depthHalfTarget.bind();
|
||||
this.depthHalfRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.quarter');
|
||||
this.depthQuarterTarget.bind();
|
||||
this.depthQuarterRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.quarter');
|
||||
|
||||
this.ssaoFramebuffer.bind();
|
||||
this.ssaoRenderable.render();
|
||||
|
||||
this.ssaoBlurFirstPassFramebuffer.bind();
|
||||
this.ssaoBlurFirstPassRenderable.render();
|
||||
|
||||
this.ssaoBlurSecondPassFramebuffer.bind();
|
||||
this.ssaoBlurSecondPassRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SSAO.render');
|
||||
this.ssao.render(camera);
|
||||
}
|
||||
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
this.outlinesTarget.bind();
|
||||
this.outlinesRenderable.render();
|
||||
this.outline.render();
|
||||
}
|
||||
|
||||
if (props.shadow.name === 'on') {
|
||||
this.shadowsTarget.bind();
|
||||
this.shadowsRenderable.render();
|
||||
this.shadow.render();
|
||||
}
|
||||
|
||||
if (toDrawingBuffer) {
|
||||
@@ -947,21 +340,8 @@ export class PostprocessingPass {
|
||||
}
|
||||
|
||||
this.background.update(camera, props.background);
|
||||
if (this.background.isEnabled(props.background)) {
|
||||
if (this.transparentBackground) {
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
} else {
|
||||
Color.toVec3Normalized(this.bgColor, backgroundColor);
|
||||
state.clearColor(this.bgColor[0], this.bgColor[1], this.bgColor[2], 1);
|
||||
}
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
state.enable(gl.BLEND);
|
||||
state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
|
||||
this.background.render();
|
||||
} else {
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
this.background.clear(props.background, this.transparentBackground, backgroundColor);
|
||||
this.background.render(props.background);
|
||||
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');
|
||||
|
||||
170
src/mol-canvas3d/passes/shadow.ts
Normal file
170
src/mol-canvas3d/passes/shadow.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, 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 { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const ShadowParams = {
|
||||
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
|
||||
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
|
||||
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
};
|
||||
|
||||
export type ShadowProps = PD.Values<typeof ShadowParams>
|
||||
|
||||
export class ShadowPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.shadow.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
private readonly renderable: ShadowsRenderable;
|
||||
|
||||
constructor(readonly webgl: WebGLContext, width: number, height: number, depthTextureOpaque: Texture) {
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
this.renderable = getShadowsRenderable(webgl, depthTextureOpaque);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, light: Light, ambientColor: Vec3, props: ShadowProps) {
|
||||
let needsUpdateShadows = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
ValueCell.update(this.renderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
|
||||
Vec4.set(this.renderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.renderable.values.uBounds, this.renderable.values.uBounds.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uMaxDistance, props.maxDistance);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uTolerance, props.tolerance);
|
||||
if (this.renderable.values.dSteps.ref.value !== props.steps) {
|
||||
ValueCell.update(this.renderable.values.dSteps, props.steps);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.update(this.renderable.values.uLightDirection, light.direction);
|
||||
ValueCell.update(this.renderable.values.uLightColor, light.color);
|
||||
if (this.renderable.values.dLightCount.ref.value !== light.count) {
|
||||
ValueCell.update(this.renderable.values.dLightCount, light.count);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
ValueCell.update(this.renderable.values.uAmbientColor, ambientColor);
|
||||
|
||||
if (needsUpdateShadows) {
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (isTimingMode) this.webgl.timer.mark('ShadowPass.render');
|
||||
this.target.bind();
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('ShadowPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
const ShadowsSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
|
||||
dSteps: DefineSpec('number'),
|
||||
uMaxDistance: UniformSpec('f'),
|
||||
uTolerance: UniformSpec('f'),
|
||||
|
||||
uLightDirection: UniformSpec('v3[]'),
|
||||
uLightColor: UniformSpec('v3[]'),
|
||||
dLightCount: DefineSpec('number'),
|
||||
uAmbientColor: UniformSpec('v3'),
|
||||
};
|
||||
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
|
||||
|
||||
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
|
||||
const width = depthTexture.getWidth();
|
||||
const height = depthTexture.getHeight();
|
||||
|
||||
const values: Values<typeof ShadowsSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
|
||||
dSteps: ValueCell.create(1),
|
||||
uMaxDistance: ValueCell.create(3.0),
|
||||
uTolerance: ValueCell.create(1.0),
|
||||
|
||||
uLightDirection: ValueCell.create([]),
|
||||
uLightColor: ValueCell.create([]),
|
||||
dLightCount: ValueCell.create(0),
|
||||
uAmbientColor: ValueCell.create(Vec3()),
|
||||
};
|
||||
|
||||
const schema = { ...ShadowsSchema };
|
||||
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
689
src/mol-canvas3d/passes/ssao.ts
Normal file
689
src/mol-canvas3d/passes/ssao.ts
Normal file
@@ -0,0 +1,689 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { deepEqual, ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { 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';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const SsaoParams = {
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
multiScale: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
levels: PD.ObjectList({
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
|
||||
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
|
||||
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
|
||||
{ radius: 2, bias: 1 },
|
||||
{ radius: 5, bias: 1 },
|
||||
{ radius: 8, bias: 1 },
|
||||
{ radius: 11, bias: 1 },
|
||||
] }),
|
||||
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
|
||||
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true }),
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
|
||||
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
|
||||
blurDepthBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
|
||||
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
transparentThreshold: PD.Numeric(0.4, { min: 0, max: 1, step: 0.05 }),
|
||||
};
|
||||
|
||||
export type SsaoProps = PD.Values<typeof SsaoParams>
|
||||
|
||||
type Levels = {
|
||||
count: number
|
||||
radius: number[]
|
||||
bias: number[]
|
||||
}
|
||||
|
||||
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
|
||||
const count = props.length;
|
||||
const { radius, bias } = levels || {
|
||||
radius: (new Array(count * 3)).fill(0),
|
||||
bias: (new Array(count * 3)).fill(0),
|
||||
};
|
||||
props = props.slice().sort((a, b) => a.radius - b.radius);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
radius[i] = Math.pow(2, p.radius);
|
||||
bias[i] = p.bias;
|
||||
}
|
||||
return { count, radius, bias };
|
||||
}
|
||||
|
||||
export class SsaoPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.occlusion.name !== 'off';
|
||||
}
|
||||
|
||||
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 downsampledDepthTargetOpaque: RenderTarget;
|
||||
private readonly downsampleDepthRenderableOpaque: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTargetOpaque: RenderTarget;
|
||||
private readonly depthHalfRenderableOpaque: 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 nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private texSize: [number, number];
|
||||
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
|
||||
}
|
||||
|
||||
private levels: { radius: number, bias: number }[];
|
||||
|
||||
private getDepthTexture() {
|
||||
return this.ssaoScale === 1 ? this.depthTextureOpaque : this.downsampledDepthTargetOpaque.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.depthTextureOpaque = depthTextureOpaque;
|
||||
this.depthTextureTransparent = depthTextureTransparent;
|
||||
|
||||
this.nSamples = 1;
|
||||
this.blurKernelSize = 1;
|
||||
this.ssaoScale = this.calcSsaoScale(1);
|
||||
this.texSize = [width, height];
|
||||
this.levels = [];
|
||||
|
||||
this.framebuffer = webgl.resources.framebuffer();
|
||||
this.blurFirstPassFramebuffer = webgl.resources.framebuffer();
|
||||
this.blurSecondPassFramebuffer = webgl.resources.framebuffer();
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
|
||||
const hw = Math.max(1, Math.floor(sw * 0.5));
|
||||
const hh = Math.max(1, Math.floor(sh * 0.5));
|
||||
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
|
||||
const filter = textureFloatLinear ? 'linear' : 'nearest';
|
||||
|
||||
this.downsampledDepthTargetOpaque = packedDepth
|
||||
? webgl.createRenderTarget(sw, sh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.downsampleDepthRenderableOpaque = createCopyRenderable(webgl, depthTextureOpaque);
|
||||
|
||||
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.depthHalfRenderableOpaque = createCopyRenderable(webgl, depthTexture);
|
||||
|
||||
this.depthQuarterTargetOpaque = packedDepth
|
||||
? webgl.createRenderTarget(qw, qh, false, 'uint8', 'linear', 'rgba')
|
||||
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
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.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');
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.texSize;
|
||||
const ssaoScale = this.calcSsaoScale(1);
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.texSize.splice(0, 2, width, height);
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
this.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));
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
|
||||
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();
|
||||
const transparentDepthTexture = this.getTransparentDepthTexture();
|
||||
|
||||
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, scene: Scene, props: SsaoProps, illuminationMode = false) {
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.texSize;
|
||||
const v = camera.viewport;
|
||||
|
||||
ValueCell.update(this.renderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
|
||||
const b = this.renderable.values.uBounds;
|
||||
const s = this.ssaoScale;
|
||||
Vec4.set(b.ref.value,
|
||||
Math.floor(v.x * s) / (w * s),
|
||||
Math.floor(v.y * s) / (h * s),
|
||||
Math.ceil((v.x + v.width) * s) / (w * s),
|
||||
Math.ceil((v.y + v.height) * s) / (h * s)
|
||||
);
|
||||
ValueCell.update(b, b.ref.value);
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBounds, b.ref.value);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBounds, b.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uNear, camera.near);
|
||||
|
||||
ValueCell.updateIfChanged(this.blurFirstPassRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.blurSecondPassRenderable.values.uFar, camera.far);
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uBlurDepthBias, props.blurDepthBias);
|
||||
|
||||
if (this.blurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
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;
|
||||
|
||||
this.nSamples = props.samples;
|
||||
ValueCell.update(this.renderable.values.uSamples, getSamples(this.nSamples));
|
||||
ValueCell.updateIfChanged(this.renderable.values.dNSamples, this.nSamples);
|
||||
}
|
||||
|
||||
const multiScale = props.multiScale.name === 'on';
|
||||
if (this.renderable.values.dMultiScale.ref.value !== multiScale) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
ValueCell.update(this.renderable.values.dMultiScale, multiScale);
|
||||
}
|
||||
|
||||
if (props.multiScale.name === 'on') {
|
||||
const mp = props.multiScale.params;
|
||||
if (!deepEqual(this.levels, mp.levels)) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.levels = mp.levels;
|
||||
const levels = getLevels(mp.levels);
|
||||
ValueCell.updateIfChanged(this.renderable.values.dLevels, levels.count);
|
||||
|
||||
ValueCell.update(this.renderable.values.uLevelRadius, levels.radius);
|
||||
ValueCell.update(this.renderable.values.uLevelBias, levels.bias);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNearThreshold, mp.nearThreshold);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFarThreshold, mp.farThreshold);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uRadius, Math.pow(2, props.radius));
|
||||
}
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);
|
||||
|
||||
if (this.blurKernelSize !== props.blurKernelSize) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
this.blurKernelSize = props.blurKernelSize;
|
||||
const kernel = getBlurKernel(this.blurKernelSize);
|
||||
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
}
|
||||
|
||||
const ssaoScale = this.calcSsaoScale(props.resolutionScale);
|
||||
if (this.ssaoScale !== ssaoScale) {
|
||||
needsUpdateSsao = true;
|
||||
needsUpdateDepthHalf = true;
|
||||
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
const sw = Math.floor(w * this.ssaoScale);
|
||||
const sh = Math.floor(h * this.ssaoScale);
|
||||
this.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);
|
||||
const qw = Math.floor(sw * 0.25);
|
||||
const qh = Math.floor(sh * 0.25);
|
||||
|
||||
this.downsampledDepthTargetOpaque.setSize(sw, sh);
|
||||
this.depthHalfTargetOpaque.setSize(hw, hh);
|
||||
this.depthQuarterTargetOpaque.setSize(qw, qh);
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
ValueCell.update(this.depthHalfRenderableOpaque.values.tColor, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepth, depthTexture);
|
||||
|
||||
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.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));
|
||||
}
|
||||
|
||||
if (needsUpdateSsao) {
|
||||
this.renderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsaoBlur) {
|
||||
this.blurFirstPassRenderable.update();
|
||||
this.blurSecondPassRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderableOpaque.update();
|
||||
this.depthHalfRenderableTransparent.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(camera: ICamera) {
|
||||
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);
|
||||
const sh = Math.ceil(height * this.ssaoScale);
|
||||
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
|
||||
if (this.ssaoScale < 1) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SSAO.downsample');
|
||||
this.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('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('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('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');
|
||||
}
|
||||
}
|
||||
|
||||
const SsaoSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
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'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uRadius: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
dMultiScale: DefineSpec('boolean'),
|
||||
dLevels: DefineSpec('number'),
|
||||
uLevelRadius: UniformSpec('f[]'),
|
||||
uLevelBias: UniformSpec('f[]'),
|
||||
uNearThreshold: UniformSpec('f'),
|
||||
uFarThreshold: UniformSpec('f'),
|
||||
};
|
||||
|
||||
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
|
||||
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture, 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),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
uRadius: ValueCell.create(Math.pow(2, 5)),
|
||||
uBias: ValueCell.create(0.8),
|
||||
|
||||
dMultiScale: ValueCell.create(false),
|
||||
dLevels: ValueCell.create(3),
|
||||
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
|
||||
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
|
||||
uNearThreshold: ValueCell.create(10.0),
|
||||
uFarThreshold: ValueCell.create(1500.0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoSchema };
|
||||
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoBlurSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uKernel: UniformSpec('f[]'),
|
||||
dOcclusionKernelSize: DefineSpec('number'),
|
||||
uBlurDepthBias: UniformSpec('f'),
|
||||
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
dOrthographic: DefineSpec('number'),
|
||||
};
|
||||
|
||||
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
|
||||
|
||||
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
|
||||
const values: Values<typeof SsaoBlurSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
|
||||
|
||||
uKernel: ValueCell.create(getBlurKernel(15)),
|
||||
dOcclusionKernelSize: ValueCell.create(15),
|
||||
uBlurDepthBias: ValueCell.create(0.5),
|
||||
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
dOrthographic: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoBlurSchema };
|
||||
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function getBlurKernel(kernelSize: number): number[] {
|
||||
const sigma = kernelSize / 3.0;
|
||||
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
|
||||
|
||||
const kernel = [];
|
||||
for (let x = 0; x < halfKernelSize; x++) {
|
||||
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
const RandomHemisphereVector: Vec3[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const v = Vec3();
|
||||
v[0] = Math.random() * 2.0 - 1.0;
|
||||
v[1] = Math.random() * 2.0 - 1.0;
|
||||
v[2] = Math.random();
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, Math.random());
|
||||
RandomHemisphereVector.push(v);
|
||||
}
|
||||
|
||||
function getSamples(nSamples: number): number[] {
|
||||
const samples = [];
|
||||
for (let i = 0; i < nSamples; i++) {
|
||||
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
|
||||
scale = 0.1 + scale * (1.0 - 0.1);
|
||||
|
||||
samples.push(RandomHemisphereVector[i][0] * scale);
|
||||
samples.push(RandomHemisphereVector[i][1] * scale);
|
||||
samples.push(RandomHemisphereVector[i][2] * scale);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
507
src/mol-canvas3d/passes/tracing.ts
Normal file
507
src/mol-canvas3d/passes/tracing.ts
Normal file
@@ -0,0 +1,507 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { DefineSpec, TextureSpec, UniformSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Renderer } from '../../mol-gl/renderer';
|
||||
import { Camera, ICamera } from '../camera';
|
||||
import { Scene } from '../../mol-gl/scene';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { trace_frag } from '../../mol-gl/shader/illumination/trace.frag';
|
||||
import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
|
||||
import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
|
||||
import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Color } from '../../mol-util/color/color';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
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;
|
||||
camera: Camera;
|
||||
scene: Scene;
|
||||
helper: Helper;
|
||||
}
|
||||
|
||||
export const TracingParams = {
|
||||
rendersPerFrame: PD.Interval([1, 16], { min: 1, max: 64, step: 1 }, { description: 'Number of rays per pixel each frame. May be adjusted to reach targetFps but will stay within given interval.' }),
|
||||
targetFps: PD.Numeric(30, { min: 0, max: 120, step: 0.1 }, { description: 'Target FPS per frame. If observed FPS is lower or higher, some parameters may get adjusted.' }),
|
||||
steps: PD.Numeric(32, { min: 1, max: 1024, step: 1 }),
|
||||
firstStepSize: PD.Numeric(0.01, { min: 0.001, max: 1, step: 0.001 }),
|
||||
refineSteps: PD.Numeric(4, { min: 0, max: 8, step: 1 }, { description: 'Number of refine steps per ray hit. May be lower to reach targetFps.' }),
|
||||
rayDistance: PD.Numeric(256, { min: 1, max: 8192, step: 1 }, { description: 'Maximum distance a ray can travel (in world units).' }),
|
||||
thicknessMode: PD.Select('auto', PD.arrayToOptions(['auto', 'fixed'] as const)),
|
||||
minThickness: PD.Numeric(0.5, { min: 0.1, max: 16, step: 0.1 }, { hideIf: p => p.thicknessMode === 'fixed' }),
|
||||
thicknessFactor: PD.Numeric(1, { min: 0.1, max: 2, step: 0.05 }, { hideIf: p => p.thicknessMode === 'fixed' }),
|
||||
thickness: PD.Numeric(4, { min: 0.1, max: 512, step: 0.1 }, { hideIf: p => p.thicknessMode === 'auto' }),
|
||||
bounces: PD.Numeric(4, { min: 1, max: 32, step: 1 }, { description: 'Number of bounces for each ray.' }),
|
||||
glow: PD.Boolean(true, { description: 'Bounced rays always get the full light. This produces a slight glowing effect.' }),
|
||||
shadowEnable: PD.Boolean(false),
|
||||
shadowSoftness: PD.Numeric(0.1, { min: 0.01, max: 1.0, step: 0.01 }),
|
||||
shadowThickness: PD.Numeric(0.5, { min: 0.1, max: 32, step: 0.1 }),
|
||||
};
|
||||
export type TracingProps = PD.Values<typeof TracingParams>
|
||||
|
||||
export class TracingPass {
|
||||
private readonly framebuffer: Framebuffer;
|
||||
|
||||
readonly colorTextureOpaque: Texture;
|
||||
readonly normalTextureOpaque: Texture;
|
||||
readonly shadedTextureOpaque: Texture;
|
||||
|
||||
private readonly thicknessTarget: RenderTarget;
|
||||
private readonly holdTarget: RenderTarget;
|
||||
readonly accumulateTarget: RenderTarget;
|
||||
readonly composeTarget: RenderTarget;
|
||||
|
||||
private readonly traceRenderable: TraceRenderable;
|
||||
private readonly accumulateRenderable: AccumulateRenderable;
|
||||
|
||||
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);
|
||||
|
||||
this.normalTextureOpaque = colorBufferHalfFloat && textureHalfFloat
|
||||
? resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
|
||||
: resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.normalTextureOpaque.define(width, height);
|
||||
|
||||
this.colorTextureOpaque = resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
|
||||
this.colorTextureOpaque.define(width, height);
|
||||
} else {
|
||||
// webgl1 requires consistent bit plane counts
|
||||
|
||||
this.shadedTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.shadedTextureOpaque.define(width, height);
|
||||
|
||||
this.normalTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.normalTextureOpaque.define(width, height);
|
||||
|
||||
this.colorTextureOpaque = resources.texture('image-float32', 'rgba', 'float', 'nearest');
|
||||
this.colorTextureOpaque.define(width, height);
|
||||
}
|
||||
|
||||
this.framebuffer = resources.framebuffer();
|
||||
|
||||
this.framebuffer.bind();
|
||||
drawBuffers!.drawBuffers([
|
||||
drawBuffers!.COLOR_ATTACHMENT0,
|
||||
drawBuffers!.COLOR_ATTACHMENT1,
|
||||
drawBuffers!.COLOR_ATTACHMENT2,
|
||||
]);
|
||||
|
||||
this.shadedTextureOpaque.attachFramebuffer(this.framebuffer, 'color0');
|
||||
this.normalTextureOpaque.attachFramebuffer(this.framebuffer, 'color1');
|
||||
this.colorTextureOpaque.attachFramebuffer(this.framebuffer, 'color2');
|
||||
|
||||
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.drawPass.depthTextureOpaque);
|
||||
this.accumulateRenderable = getAccumulateRenderable(webgl, this.holdTarget.texture);
|
||||
}
|
||||
|
||||
private renderInput(renderer: Renderer, camera: ICamera, scene: Scene, props: TracingProps) {
|
||||
if (isTimingMode) this.webgl.timer.mark('TracePass.renderInput');
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
this.framebuffer.bind();
|
||||
this.drawPass.depthTextureOpaque.attachFramebuffer(this.framebuffer, 'depth');
|
||||
renderer.clear(true);
|
||||
renderer.renderTracing(scene.primitives, camera);
|
||||
|
||||
//
|
||||
|
||||
if (props.thicknessMode === 'auto') {
|
||||
this.thicknessTarget.bind();
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
|
||||
renderer.renderDepthOpaqueBack(scene.primitives, camera);
|
||||
}
|
||||
if (isTimingMode) this.webgl.timer.markEnd('TracePass.renderInput');
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const w = this.composeTarget.getWidth();
|
||||
const h = this.composeTarget.getHeight();
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.thicknessTarget.setSize(width, height);
|
||||
this.holdTarget.setSize(width, height);
|
||||
this.accumulateTarget.setSize(width, height);
|
||||
this.composeTarget.setSize(width, height);
|
||||
|
||||
this.colorTextureOpaque.define(width, height);
|
||||
this.normalTextureOpaque.define(width, height);
|
||||
this.shadedTextureOpaque.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));
|
||||
}
|
||||
}
|
||||
|
||||
private clearAdjustedProps = true;
|
||||
|
||||
reset(clearAdjustedProps = false) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
this.accumulateTarget.bind();
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
this.composeTarget.bind();
|
||||
state.clearColor(0, 0, 0, 0);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
|
||||
if (clearAdjustedProps) {
|
||||
this.prevTime = 0;
|
||||
this.currTime = 0;
|
||||
this.clearAdjustedProps = true;
|
||||
}
|
||||
}
|
||||
|
||||
private prevTime = 0;
|
||||
private currTime = 0;
|
||||
private rendersPerFrame = 1;
|
||||
private refineSteps = 1;
|
||||
private steps = 16;
|
||||
|
||||
private increaseAdjustedProps(props: TracingProps) {
|
||||
this.steps += 1;
|
||||
if (this.steps > props.steps) {
|
||||
this.refineSteps += 1;
|
||||
}
|
||||
if (this.refineSteps > props.refineSteps) {
|
||||
this.rendersPerFrame += 1;
|
||||
}
|
||||
}
|
||||
|
||||
private decreaseAdjustedProps(props: TracingProps) {
|
||||
const minRefineSteps = Math.min(1, props.refineSteps);
|
||||
this.rendersPerFrame -= 1;
|
||||
if (this.rendersPerFrame < 1) {
|
||||
this.refineSteps -= 1;
|
||||
}
|
||||
if (this.refineSteps < minRefineSteps) {
|
||||
this.steps -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
private getAdjustedProps(props: TracingProps, iteration: number) {
|
||||
this.currTime = now();
|
||||
const minRefineSteps = Math.min(1, props.refineSteps);
|
||||
const minSteps = Math.round(props.steps / 2);
|
||||
|
||||
if (this.clearAdjustedProps) {
|
||||
this.rendersPerFrame = props.rendersPerFrame[0];
|
||||
this.refineSteps = minRefineSteps;
|
||||
this.steps = minSteps;
|
||||
this.clearAdjustedProps = false;
|
||||
}
|
||||
|
||||
if (iteration > 0) {
|
||||
const targetTimeMs = 1000 / props.targetFps;
|
||||
const deltaTime = this.currTime - this.prevTime;
|
||||
let f = Math.round(deltaTime / targetTimeMs);
|
||||
if (f >= 2) {
|
||||
while (f > 0) {
|
||||
this.decreaseAdjustedProps(props);
|
||||
f -= 1;
|
||||
}
|
||||
} else if (deltaTime < targetTimeMs) {
|
||||
this.increaseAdjustedProps(props);
|
||||
} else if (deltaTime > targetTimeMs + 0.5) {
|
||||
this.decreaseAdjustedProps(props);
|
||||
}
|
||||
}
|
||||
|
||||
this.prevTime = this.currTime;
|
||||
this.rendersPerFrame = clamp(this.rendersPerFrame, props.rendersPerFrame[0], props.rendersPerFrame[1]);
|
||||
this.refineSteps = clamp(this.refineSteps, minRefineSteps, props.refineSteps);
|
||||
this.steps = clamp(this.steps, minSteps, props.steps);
|
||||
|
||||
return {
|
||||
rendersPerFrame: iteration === 0 ? Math.ceil(this.rendersPerFrame / 2) : this.rendersPerFrame,
|
||||
refineSteps: iteration === 0 ? minRefineSteps : this.refineSteps,
|
||||
steps: iteration === 0 ? minSteps : this.steps,
|
||||
};
|
||||
}
|
||||
|
||||
render(ctx: RenderContext, transparentBackground: boolean, props: TracingProps, iteration: number, forceRenderInput: boolean) {
|
||||
const { rendersPerFrame, refineSteps, steps } = this.getAdjustedProps(props, iteration);
|
||||
|
||||
if (isTimingMode) {
|
||||
this.webgl.timer.mark('TracePass.render', {
|
||||
note: `${rendersPerFrame} rendersPerFrame, ${refineSteps} refineSteps, ${steps} steps`
|
||||
});
|
||||
}
|
||||
|
||||
const { renderer, camera, scene } = ctx;
|
||||
const { gl, state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
|
||||
if (iteration === 0 || forceRenderInput) {
|
||||
// render color & depth
|
||||
renderer.setTransparentBackground(transparentBackground);
|
||||
renderer.setDrawingBufferSize(this.composeTarget.getWidth(), this.composeTarget.getHeight());
|
||||
renderer.setPixelRatio(this.webgl.pixelRatio);
|
||||
renderer.setViewport(x, y, width, height);
|
||||
renderer.update(camera, scene);
|
||||
this.renderInput(renderer, camera, scene, props);
|
||||
}
|
||||
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.disable(gl.CULL_FACE);
|
||||
state.depthMask(false);
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
const [w, h] = this.traceRenderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
const ambientColor = Vec3();
|
||||
Vec3.scale(ambientColor, Color.toArrayNormalized(renderer.props.ambientColor, ambientColor, 0), renderer.props.ambientIntensity);
|
||||
const lightStrength = Vec3.clone(ambientColor);
|
||||
for (let i = 0, il = renderer.light.count; i < il; ++i) {
|
||||
const light = Vec3.fromArray(Vec3(), renderer.light.color, i * 3);
|
||||
Vec3.add(lightStrength, lightStrength, light);
|
||||
}
|
||||
|
||||
// trace
|
||||
this.holdTarget.bind();
|
||||
let needsUpdateTrace = false;
|
||||
ValueCell.update(this.traceRenderable.values.uFrameNo, iteration);
|
||||
if (this.traceRenderable.values.dRendersPerFrame.ref.value !== rendersPerFrame) {
|
||||
ValueCell.update(this.traceRenderable.values.dRendersPerFrame, rendersPerFrame);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.update(this.traceRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.traceRenderable.values.uInvProjection, invProjection);
|
||||
Vec4.set(this.traceRenderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.traceRenderable.values.uBounds, this.traceRenderable.values.uBounds.ref.value);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uFogFar, camera.fogFar);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uFogNear, camera.fogNear);
|
||||
ValueCell.update(this.traceRenderable.values.uFogColor, Color.toVec3Normalized(this.traceRenderable.values.uFogColor.ref.value, renderer.props.backgroundColor));
|
||||
if (this.traceRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
ValueCell.update(this.traceRenderable.values.dOrthographic, orthographic);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.update(this.traceRenderable.values.uLightDirection, renderer.light.direction);
|
||||
ValueCell.update(this.traceRenderable.values.uLightColor, renderer.light.color);
|
||||
if (this.traceRenderable.values.dLightCount.ref.value !== renderer.light.count) {
|
||||
ValueCell.update(this.traceRenderable.values.dLightCount, renderer.light.count);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.update(this.traceRenderable.values.uAmbientColor, ambientColor);
|
||||
ValueCell.update(this.traceRenderable.values.uLightStrength, lightStrength);
|
||||
if (this.traceRenderable.values.dGlow.ref.value !== props.glow) {
|
||||
ValueCell.update(this.traceRenderable.values.dGlow, props.glow);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
if (this.traceRenderable.values.dBounces.ref.value !== props.bounces) {
|
||||
ValueCell.update(this.traceRenderable.values.dBounces, props.bounces);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
if (this.traceRenderable.values.dSteps.ref.value !== steps) {
|
||||
ValueCell.update(this.traceRenderable.values.dSteps, steps);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
if (this.traceRenderable.values.dFirstStepSize.ref.value !== props.firstStepSize) {
|
||||
ValueCell.update(this.traceRenderable.values.dFirstStepSize, props.firstStepSize);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
if (this.traceRenderable.values.dRefineSteps.ref.value !== refineSteps) {
|
||||
ValueCell.update(this.traceRenderable.values.dRefineSteps, refineSteps);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uRayDistance, props.rayDistance);
|
||||
if (this.traceRenderable.values.dThicknessMode.ref.value !== props.thicknessMode) {
|
||||
ValueCell.update(this.traceRenderable.values.dThicknessMode, props.thicknessMode);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uMinThickness, props.minThickness);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uThicknessFactor, props.thicknessFactor);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uThickness, props.thickness);
|
||||
if (this.traceRenderable.values.dShadowEnable.ref.value !== props.shadowEnable) {
|
||||
ValueCell.update(this.traceRenderable.values.dShadowEnable, props.shadowEnable);
|
||||
needsUpdateTrace = true;
|
||||
}
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowSoftness, props.shadowSoftness);
|
||||
ValueCell.updateIfChanged(this.traceRenderable.values.uShadowThickness, props.shadowThickness);
|
||||
if (needsUpdateTrace) this.traceRenderable.update();
|
||||
if (isTimingMode) this.webgl.timer.mark('TracePass.renderTrace');
|
||||
this.traceRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('TracePass.renderTrace');
|
||||
|
||||
// accumulate
|
||||
this.accumulateTarget.bind();
|
||||
this.accumulateRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('TracePass.render');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const TraceSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tNormal: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tShaded: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tThickness: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tAccumulate: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uFogNear: UniformSpec('f'),
|
||||
uFogFar: UniformSpec('f'),
|
||||
uFogColor: UniformSpec('v3'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uLightDirection: UniformSpec('v3[]'),
|
||||
uLightColor: UniformSpec('v3[]'),
|
||||
dLightCount: DefineSpec('number'),
|
||||
uAmbientColor: UniformSpec('v3'),
|
||||
uLightStrength: UniformSpec('v3'),
|
||||
|
||||
uFrameNo: UniformSpec('i'),
|
||||
dRendersPerFrame: DefineSpec('number'),
|
||||
|
||||
dGlow: DefineSpec('boolean'),
|
||||
dBounces: DefineSpec('number'),
|
||||
dSteps: DefineSpec('number'),
|
||||
dFirstStepSize: DefineSpec('number'),
|
||||
dRefineSteps: DefineSpec('number'),
|
||||
uRayDistance: UniformSpec('f'),
|
||||
|
||||
dThicknessMode: DefineSpec('string'),
|
||||
uMinThickness: UniformSpec('f'),
|
||||
uThicknessFactor: UniformSpec('f'),
|
||||
uThickness: UniformSpec('f'),
|
||||
|
||||
dShadowEnable: DefineSpec('boolean'),
|
||||
uShadowSoftness: UniformSpec('f'),
|
||||
uShadowThickness: UniformSpec('f'),
|
||||
};
|
||||
const TraceShaderCode = ShaderCode('trace', quad_vert, trace_frag);
|
||||
type TraceRenderable = ComputeRenderable<Values<typeof TraceSchema>>
|
||||
|
||||
function getTraceRenderable(ctx: WebGLContext, colorTexture: Texture, normalTexture: Texture, shadedTexture: Texture, thicknessTexture: Texture, accumulateTexture: Texture, depthTexture: Texture): TraceRenderable {
|
||||
const values: Values<typeof TraceSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tNormal: ValueCell.create(normalTexture),
|
||||
tShaded: ValueCell.create(shadedTexture),
|
||||
tThickness: ValueCell.create(thicknessTexture),
|
||||
tAccumulate: ValueCell.create(accumulateTexture),
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uFogNear: ValueCell.create(10000),
|
||||
uFogFar: ValueCell.create(10000),
|
||||
uFogColor: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uLightDirection: ValueCell.create([]),
|
||||
uLightColor: ValueCell.create([]),
|
||||
dLightCount: ValueCell.create(0),
|
||||
uAmbientColor: ValueCell.create(Vec3()),
|
||||
uLightStrength: ValueCell.create(Vec3.create(1, 1, 1)),
|
||||
|
||||
uFrameNo: ValueCell.create(0),
|
||||
dRendersPerFrame: ValueCell.create(1),
|
||||
|
||||
dGlow: ValueCell.create(true),
|
||||
dBounces: ValueCell.create(4),
|
||||
dSteps: ValueCell.create(32),
|
||||
dFirstStepSize: ValueCell.create(0.01),
|
||||
dRefineSteps: ValueCell.create(4),
|
||||
uRayDistance: ValueCell.create(256),
|
||||
|
||||
dThicknessMode: ValueCell.create('auto'),
|
||||
uMinThickness: ValueCell.create(0.5),
|
||||
uThicknessFactor: ValueCell.create(1),
|
||||
uThickness: ValueCell.create(4),
|
||||
|
||||
dShadowEnable: ValueCell.create(false),
|
||||
uShadowSoftness: ValueCell.create(0.1),
|
||||
uShadowThickness: ValueCell.create(0.1),
|
||||
};
|
||||
|
||||
const schema = { ...TraceSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', TraceShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const AccumulateSchema = {
|
||||
...QuadSchema,
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
uWeight: UniformSpec('f'),
|
||||
};
|
||||
const AccumulateShaderCode = ShaderCode('accumulate', quad_vert, accumulate_frag);
|
||||
type AccumulateRenderable = ComputeRenderable<Values<typeof AccumulateSchema>>
|
||||
|
||||
function getAccumulateRenderable(ctx: WebGLContext, colorTexture: Texture): AccumulateRenderable {
|
||||
const values: Values<typeof AccumulateSchema> = {
|
||||
...QuadValues,
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
uWeight: ValueCell.create(1.0),
|
||||
};
|
||||
|
||||
const schema = { ...AccumulateSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', AccumulateShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -87,6 +87,7 @@ export namespace BaseGeometry {
|
||||
material: Material.getParam(),
|
||||
clip: PD.Group(Clip.Params),
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
density: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }, { description: 'Density value to estimate object thickness.' }),
|
||||
instanceGranularity: PD.Boolean(false, { description: 'Use instance granularity for marker, transparency, clipping, overpaint, substance data to save memory.' }),
|
||||
lod: PD.Vec3(Vec3(), undefined, { ...CullingLodCategory, description: 'Level of detail.', fieldLabels: { x: 'Min Distance', y: 'Max Distance', z: 'Overlap (Shader)' } }),
|
||||
cellSize: PD.Numeric(200, { min: 0, max: 5000, step: 100 }, { ...CullingLodCategory, description: 'Instance grid cell size.' }),
|
||||
@@ -118,6 +119,7 @@ export namespace BaseGeometry {
|
||||
uRoughness: ValueCell.create(props.material.roughness),
|
||||
uBumpiness: ValueCell.create(props.material.bumpiness),
|
||||
uEmissive: ValueCell.create(props.emissive),
|
||||
uDensity: ValueCell.create(props.density),
|
||||
dLightCount: ValueCell.create(1),
|
||||
dColorMarker: ValueCell.create(true),
|
||||
|
||||
@@ -140,6 +142,7 @@ export namespace BaseGeometry {
|
||||
ValueCell.updateIfChanged(values.uRoughness, props.material.roughness);
|
||||
ValueCell.updateIfChanged(values.uBumpiness, props.material.bumpiness);
|
||||
ValueCell.updateIfChanged(values.uEmissive, props.emissive);
|
||||
ValueCell.updateIfChanged(values.uDensity, props.density);
|
||||
|
||||
const clip = Clip.getClip(props.clip);
|
||||
ValueCell.updateIfChanged(values.dClipObjectCount, clip.objects.count);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user