mirror of
https://github.com/molstar/molstar.git
synced 2026-06-07 07:04:22 +08:00
Compare commits
246 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
2b2dfd9245 | ||
|
|
678790efa3 | ||
|
|
a121c5e2cd | ||
|
|
47b242244e | ||
|
|
2b9d3fd33a | ||
|
|
81404036a2 | ||
|
|
e2dc15cf0f | ||
|
|
365a91879f | ||
|
|
e47e0eb51a | ||
|
|
390046e38f | ||
|
|
b8eb5191a2 | ||
|
|
4cc416ca28 | ||
|
|
f4b2458390 | ||
|
|
1cad6eef74 | ||
|
|
2923be6006 | ||
|
|
70959641a1 | ||
|
|
051608f56c | ||
|
|
71c1a4e85b | ||
|
|
ba06c9e413 | ||
|
|
8e4dfd1ffd | ||
|
|
fac8aa529f | ||
|
|
d35b4b5e62 | ||
|
|
dd1789478b | ||
|
|
af27a00a01 | ||
|
|
17cea8f99c | ||
|
|
4f6d5a7dc7 | ||
|
|
857972653e | ||
|
|
98ff0f5c55 | ||
|
|
43803a91ea | ||
|
|
8a1bab8bcb | ||
|
|
17a47faaff | ||
|
|
f793167e91 | ||
|
|
d1c2c8e837 | ||
|
|
5d7ef8196e | ||
|
|
e0715cbf5c | ||
|
|
1af8522de3 | ||
|
|
c6becd5741 | ||
|
|
0ca368f29f | ||
|
|
5039a448ad | ||
|
|
9ef38f02c9 | ||
|
|
66f4ff1140 | ||
|
|
cc077656a9 | ||
|
|
3ef1a2ec0a | ||
|
|
144bf6954e | ||
|
|
50e5538148 | ||
|
|
efe95f92c7 | ||
|
|
09f858a755 | ||
|
|
e7082d4ccc | ||
|
|
732a8f4bd0 | ||
|
|
82ca06b29e | ||
|
|
a05429f13f | ||
|
|
377de7ad40 | ||
|
|
74f4d00c8d | ||
|
|
be3825372e | ||
|
|
d62c5c9050 | ||
|
|
9ba5112beb | ||
|
|
048658ee39 | ||
|
|
2918081dd9 | ||
|
|
1a67868c07 | ||
|
|
525dfaddd2 | ||
|
|
8aa12c0d31 | ||
|
|
8cb464a686 | ||
|
|
e0371d7e32 | ||
|
|
e7da6bc194 | ||
|
|
933869b5e1 | ||
|
|
5d6adc46fe | ||
|
|
b8a98efcaf | ||
|
|
53b358f70a | ||
|
|
bce9e5b0ad | ||
|
|
cf7f9d6aba | ||
|
|
6af1bc5def | ||
|
|
936808e271 | ||
|
|
8b985d0424 | ||
|
|
b290cf121a | ||
|
|
89df6cec42 | ||
|
|
fc5fc7fcdb | ||
|
|
a1de5bb304 | ||
|
|
25c8a41e91 | ||
|
|
959249b572 | ||
|
|
44610b8b1a | ||
|
|
4070453209 | ||
|
|
cf2193f4fc | ||
|
|
bfc0a3d1fe | ||
|
|
53a2155d8c | ||
|
|
f22121521b | ||
|
|
755655d067 | ||
|
|
41ab186fd2 | ||
|
|
9039c653cb | ||
|
|
dde3f4ecff | ||
|
|
64cd05cc14 | ||
|
|
593e8f4993 | ||
|
|
d2d9eb622f | ||
|
|
ce9883517f | ||
|
|
bc2afe1d68 | ||
|
|
5a2ee03b48 | ||
|
|
2d86c76788 | ||
|
|
7f8995a4d8 | ||
|
|
64ab8bf78d | ||
|
|
bf9663e177 | ||
|
|
fb729446e2 | ||
|
|
3e4082bf6e | ||
|
|
5af6a3e967 | ||
|
|
e718835042 | ||
|
|
70f0804e26 | ||
|
|
e613a90754 | ||
|
|
dca6affc84 | ||
|
|
af33516107 | ||
|
|
7148c7197b | ||
|
|
d2192d609a | ||
|
|
46ad8f495f | ||
|
|
65b2b69a64 | ||
|
|
0d9d173ef4 | ||
|
|
6eaf8e1911 | ||
|
|
b11e24cd06 | ||
|
|
1a1bce8193 | ||
|
|
b67f7271fc | ||
|
|
837b838766 | ||
|
|
7dd42421e2 | ||
|
|
258dc637fc | ||
|
|
d6c594395c | ||
|
|
746173fe33 | ||
|
|
e9de12e6a2 | ||
|
|
3bfebceaea | ||
|
|
443fc9c60c | ||
|
|
3220ab6118 | ||
|
|
5c882f1aa5 | ||
|
|
2320518b87 | ||
|
|
2124bead5e | ||
|
|
a6077c7263 | ||
|
|
6f478a3eb3 | ||
|
|
a36c2feee4 | ||
|
|
a5e2946aa6 | ||
|
|
00428254a8 | ||
|
|
53a57530c5 | ||
|
|
5ee6bee130 | ||
|
|
734b6001c2 | ||
|
|
e88c2df42f | ||
|
|
79e6a4c95d | ||
|
|
2f56b9c491 |
@@ -1,3 +1,4 @@
|
||||
node_modules/*
|
||||
build/*
|
||||
docs/site/*
|
||||
lib/*
|
||||
91
CHANGELOG.md
91
CHANGELOG.md
@@ -5,7 +5,86 @@ Note that since we don't clearly distinguish between a public and private interf
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v4.2.0] - 2023-04-05
|
||||
## [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
|
||||
- Adjust Quick Styles panel button labels
|
||||
- Improve camera interpolation code (interpolate camera rotation instead of just position)
|
||||
- Mesoscale Explorer
|
||||
- Add `illustrative` coloring option
|
||||
- Press 'C' to toggle between center and zoom & center on click
|
||||
- Add entities selection description
|
||||
- Clicking a leaf node in the right panel tree will center each instance in turn
|
||||
- Add measurement controls to right panel
|
||||
- Mouse left click on label with snapshot key will load snapshot
|
||||
- Mouse hover over label with protein name highlight entities with the same name
|
||||
- Custom ViewportSnapshotDescription with custom MarkdowAnchor
|
||||
- \# other snapshots with a given key \[...](#key)
|
||||
- i highlight a protein with a given NAME \[...](iNAME)
|
||||
- g highlight a group with a given group type and group name \[...](ggrouptype.groupname)
|
||||
- h URLs with a given link \[...](http...)
|
||||
- Snapshot description panel window size and text can be resized and hidden with new icons
|
||||
- Add styles controls to right panel
|
||||
- Add viewport settings to left panel
|
||||
- Add app info component to left panel with interactive tour and doc link
|
||||
- Fixes SSAO edge artifacts (#1122)
|
||||
- Add `reuseOcclusion` parameter to multi-sample pass
|
||||
- Add `blurDepthBias` parameter to occlusion pass
|
||||
- Handle near clip in SSAO blur
|
||||
- Support reading score from B-factor in pLDDT color theme
|
||||
- Add Cel-shading support
|
||||
- `celShaded` geometry parameter
|
||||
- `celSteps` renderer parameter
|
||||
- Add the ability to customize the Snapshot Description component via `PluginUISpec.components.viewport.snapshotDescription`
|
||||
- Add `doNotDisposeCanvas3DContext` option to `PluginContext.dispose`
|
||||
- Remove support for density data from edmaps.rcsb.org
|
||||
|
||||
## [v4.3.0] - 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] - 2024-05-04
|
||||
|
||||
- Add emissive material support
|
||||
- Add bloom post-processing
|
||||
@@ -23,7 +102,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
|
||||
@@ -34,13 +113,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)
|
||||
@@ -76,7 +155,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
|
||||
@@ -90,7 +169,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
|
||||
|
||||
@@ -107,6 +107,7 @@ entity.id
|
||||
entity.type
|
||||
entity.src_method
|
||||
entity.pdbx_description
|
||||
entity.pdbx_parent_entity_id
|
||||
entity.formula_weight
|
||||
entity.pdbx_number_of_molecules
|
||||
entity.details
|
||||
@@ -262,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
|
||||
@@ -366,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
|
||||
|
||||
|
118
docs/docs/extensions/tunnels.md
Normal file
118
docs/docs/extensions/tunnels.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Tunnel Visualization Extension
|
||||
This documentation outlines the usage of the Mol* extension for visualizing tunnels in molecular structures. The extension integrates with Mol* to render 3D representations of tunnels using specified data sources and properties.
|
||||
|
||||
The extension is a key component in ChannelsDB (https://channelsdb2.biodata.ceitec.cz/), enabling users to visualize tunnels within molecules directly from the database. While it is used with ChannelsDB, users can also input their own data or connect to different databases, ensuring versatility across various research environments.
|
||||
|
||||
## Data Types
|
||||
The primary data types involved in tunnel visualization are:
|
||||
|
||||
### Tunnel
|
||||
A Tunnel object contains the actual tunnel data necessary for visualization. It consists of:
|
||||
|
||||
- `data`: An array of `Profile` objects that describe the tunnel at various points.
|
||||
- `props`: Properties such as the tunnel's type, ID, and optional labels or descriptions.
|
||||
|
||||
### Profile
|
||||
A `Profile` object in a `Tunnel` holds detailed geometric and physical properties of a tunnel at specific points along its length. These properties include:
|
||||
|
||||
- `Charge`: The electric charge at a specific point in the tunnel.
|
||||
- `Radius`: The overall radius of the tunnel at this point.
|
||||
- `FreeRadius`: The radius of the tunnel not obstructed by any molecular elements.
|
||||
- `T`: Temperature factor or a similar property related to the point.
|
||||
- `Distance`: Distance along the tunnel's path from the start.
|
||||
- `X`, `Y`, `Z`: Coordinates of the point in 3D space.
|
||||
|
||||
These profiles are crucial for understanding the physical and chemical environment inside the tunnel, allowing for detailed analysis and visualization.
|
||||
|
||||
Example:
|
||||
```json
|
||||
"Profile": [
|
||||
{
|
||||
"Radius": 1.49,
|
||||
"FreeRadius": 1.49,
|
||||
"T": 0,
|
||||
"Distance": 0,
|
||||
"X": -19.152,
|
||||
"Y": -22.654,
|
||||
"Z": -13.034,
|
||||
"Charge": 0
|
||||
},
|
||||
{
|
||||
"Radius": 1.524,
|
||||
"FreeRadius": 1.524,
|
||||
"T": 0.00625,
|
||||
"Distance": 0.087,
|
||||
"X": -19.162,
|
||||
"Y": -22.596,
|
||||
"Z": -12.969,
|
||||
"Charge": 0
|
||||
},
|
||||
{
|
||||
"Radius": 1.56,
|
||||
"FreeRadius": 1.56,
|
||||
"T": 0.0125,
|
||||
"Distance": 0.174,
|
||||
"X": -19.171,
|
||||
"Y": -22.539,
|
||||
"Z": -12.905,
|
||||
"Charge": 0
|
||||
}
|
||||
]
|
||||
```
|
||||
## Transformers Usage
|
||||
The extension uses several transformations to process and visualize tunnel data:
|
||||
|
||||
### Tunnels Data Transformer
|
||||
- `Purpose`: Converts a collection of Tunnel data into a state object.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.toRoot().apply(TunnelsFromRawData, { data: tunnels });
|
||||
```
|
||||
|
||||
### Tunnel Data Provider
|
||||
- `Purpose`: Converts single Tunnel data into a state object for individual processing.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.toRoot().apply(TunnelFromRawData, {
|
||||
data: {
|
||||
data: tunnel.Profile,
|
||||
props: { id: tunnel.Id, type: tunnel.Type }
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Tunnel Shape Provider
|
||||
- `Purpose`: Provides the shapes for rendering the tunnel based on WebGL context and shape parameters.
|
||||
- `Usage`:
|
||||
```typescript
|
||||
update.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
}).apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
|
||||
## Visualization Examples
|
||||
To help users understand how to use these transformations in practice, include detailed examples:
|
||||
|
||||
### Visualizing Multiple Tunnels
|
||||
This example ([runVisualizeTunnels](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L19)) demonstrates how to visualize multiple tunnels from a fetched dataset.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, { webgl })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
|
||||
### Visualizing a Single Tunnel
|
||||
This example ([runVisualizeTunnel](../../../src/extensions/sb-ncbr/tunnels/examples.ts#L46)) shows how to visualize a single tunnel.
|
||||
```typescript
|
||||
update.toRoot()
|
||||
.apply(TunnelFromRawData, {
|
||||
data: {
|
||||
data: tunnel.Profile,
|
||||
props: { id: tunnel.Id, type: tunnel.Type }
|
||||
}
|
||||
})
|
||||
.apply(TunnelShapeProvider, { webgl })
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
```
|
||||
@@ -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`:
|
||||

|
||||
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 |
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'
|
||||
@@ -53,6 +54,7 @@ nav:
|
||||
- Extensions:
|
||||
- MolViewSpec: 'extensions/mvs/index.md'
|
||||
- wwPDB StructConn: 'extensions/struct-conn.md'
|
||||
- Tunnels: 'extensions/tunnels.md'
|
||||
- Misc:
|
||||
- Interesting PDB entries: misc/interesting-pdb-entries.md
|
||||
repo_url: https://github.com/molstar/docs
|
||||
1180
package-lock.json
generated
1180
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
42
package.json
42
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "molstar",
|
||||
"version": "4.2.0",
|
||||
"version": "4.6.0",
|
||||
"description": "A comprehensive macromolecular library.",
|
||||
"homepage": "https://github.com/molstar/molstar#readme",
|
||||
"repository": {
|
||||
@@ -106,23 +106,27 @@
|
||||
"Yakov Pechersky <ffxen158@gmail.com>",
|
||||
"Christian Dominguez <christian.99dominguez@gmail.com>",
|
||||
"Cai Huiyu <szmun.caihy@gmail.com>",
|
||||
"Ryan DiRisio <rjdiris@gmail.com>"
|
||||
"Ryan DiRisio <rjdiris@gmail.com>",
|
||||
"Dušan Veľký <dvelky@mail.muni.cz>",
|
||||
"Neli Fonseca <neli@ebi.ac.uk>",
|
||||
"Paul Pillot <paul.pillot@tandemai.com>",
|
||||
"Herman Bergwerf <post@hbergwerf.nl>"
|
||||
],
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/gl": "^6.0.5",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/react": "^18.3.1",
|
||||
"@types/pngjs": "^6.0.5",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.8.0",
|
||||
"@typescript-eslint/parser": "^7.8.0",
|
||||
"@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",
|
||||
"css-loader": "^7.1.1",
|
||||
"css-loader": "^7.1.2",
|
||||
"eslint": "^8.57.0",
|
||||
"extra-watch-webpack-plugin": "^1.0.3",
|
||||
"file-loader": "^6.2.0",
|
||||
@@ -130,19 +134,19 @@
|
||||
"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.1",
|
||||
"path-browserify": "^1.0.1",
|
||||
"raw-loader": "^4.0.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"sass": "^1.76.0",
|
||||
"sass-loader": "^14.2.1",
|
||||
"simple-git": "^3.24.0",
|
||||
"sass": "^1.77.8",
|
||||
"sass-loader": "^16.0.1",
|
||||
"simple-git": "^3.25.0",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"style-loader": "^4.0.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
"typescript": "^5.4.5",
|
||||
"webpack": "^5.91.0",
|
||||
"ts-jest": "^29.2.5",
|
||||
"typescript": "^5.5.4",
|
||||
"webpack": "^5.94.0",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -150,9 +154,9 @@
|
||||
"@types/benchmark": "^2.1.5",
|
||||
"@types/compression": "1.7.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^18.19.31",
|
||||
"@types/node": "^18.19.47",
|
||||
"@types/node-fetch": "^2.6.11",
|
||||
"@types/swagger-ui-dist": "3.30.4",
|
||||
"@types/swagger-ui-dist": "3.30.5",
|
||||
"argparse": "^2.0.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"compression": "^1.7.4",
|
||||
@@ -160,13 +164,13 @@
|
||||
"express": "^4.19.2",
|
||||
"h264-mp4-encoder": "^1.0.12",
|
||||
"immer": "^10.1.1",
|
||||
"immutable": "^4.3.5",
|
||||
"immutable": "^4.3.7",
|
||||
"io-ts": "^2.2.21",
|
||||
"node-fetch": "^2.7.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rxjs": "^7.8.1",
|
||||
"swagger-ui-dist": "^5.17.2",
|
||||
"tslib": "^2.6.2",
|
||||
"swagger-ui-dist": "^5.17.14",
|
||||
"tslib": "^2.7.0",
|
||||
"util.promisify": "^1.1.2",
|
||||
"xhr2": "^0.2.1"
|
||||
},
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2020-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -46,6 +46,7 @@ function occlusionStyle(plugin: PluginContext) {
|
||||
...plugin.canvas3d!.props.postprocessing,
|
||||
occlusion: { name: 'on', params: {
|
||||
blurKernelSize: 15,
|
||||
blurDepthBias: 0.5,
|
||||
multiScale: { name: 'off', params: {} },
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
|
||||
@@ -30,6 +30,7 @@ import { Asset } from '../../mol-util/assets';
|
||||
import { AnimateCameraSpin } from '../../mol-plugin-state/animation/built-in/camera-spin';
|
||||
import { AnimateCameraRock } from '../../mol-plugin-state/animation/built-in/camera-rock';
|
||||
import { AnimateStateSnapshots } from '../../mol-plugin-state/animation/built-in/state-snapshots';
|
||||
import { MesoViewportSnapshotDescription } from './ui/entities';
|
||||
|
||||
export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
|
||||
export { setDebugMode, setProductionMode, setTimingMode, consoleStats } from '../../mol-util/debug';
|
||||
@@ -47,6 +48,7 @@ export type MesoscaleExplorerState = {
|
||||
examples?: ExampleEntry[],
|
||||
graphicsMode: GraphicsMode,
|
||||
stateRef?: string,
|
||||
driver?: any,
|
||||
stateCache: { [k: string]: any },
|
||||
}
|
||||
|
||||
@@ -90,7 +92,8 @@ const DefaultMesoscaleExplorerOptions = {
|
||||
emdbProvider: PluginConfig.Download.DefaultEmdbProvider.defaultValue,
|
||||
saccharideCompIdMapType: 'default' as SaccharideCompIdMapType,
|
||||
|
||||
graphicsMode: 'quality' as GraphicsMode
|
||||
graphicsMode: 'quality' as GraphicsMode,
|
||||
driver: undefined
|
||||
};
|
||||
type MesoscaleExplorerOptions = typeof DefaultMesoscaleExplorerOptions;
|
||||
|
||||
@@ -170,6 +173,9 @@ export class MesoscaleExplorer {
|
||||
right: RightPanel,
|
||||
},
|
||||
remoteState: 'none',
|
||||
viewport: {
|
||||
snapshotDescription: MesoViewportSnapshotDescription,
|
||||
}
|
||||
},
|
||||
config: [
|
||||
[PluginConfig.General.DisableAntialiasing, o.disableAntialiasing],
|
||||
@@ -207,7 +213,12 @@ export class MesoscaleExplorer {
|
||||
onBeforeUIRender: async plugin => {
|
||||
let examples: MesoscaleExplorerState['examples'] = undefined;
|
||||
try {
|
||||
examples = await plugin.fetch({ url: './examples/list.json', type: 'json' }).run();
|
||||
examples = await plugin.fetch({ url: '../examples/list.json', type: 'json' }).run();
|
||||
// extend the array with file tour.json if it exists
|
||||
const tour = await plugin.fetch({ url: '../examples/tour.json', type: 'json' }).run();
|
||||
if (tour) {
|
||||
examples = examples?.concat(tour);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
@@ -215,6 +226,7 @@ export class MesoscaleExplorer {
|
||||
(plugin.customState as MesoscaleExplorerState) = {
|
||||
examples,
|
||||
graphicsMode: o.graphicsMode,
|
||||
driver: o.driver,
|
||||
stateCache: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@ import { ButtonsType, ModifiersKeys } from '../../../mol-util/input/input-observ
|
||||
import { Binding } from '../../../mol-util/binding';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { StructureElement } from '../../../mol-model/structure';
|
||||
|
||||
const B = ButtonsType;
|
||||
const M = ModifiersKeys;
|
||||
const Trigger = Binding.Trigger;
|
||||
const Key = Binding.TriggerKey;
|
||||
|
||||
const DefaultMesoFocusLociBindings = {
|
||||
clickCenter: Binding([
|
||||
@@ -23,13 +25,14 @@ const DefaultMesoFocusLociBindings = {
|
||||
clickCenterFocus: Binding([
|
||||
Trigger(B.Flag.Secondary, M.create()),
|
||||
], 'Camera center and focus', 'Click element using ${triggers}'),
|
||||
keyCenterOnly: Binding([Key('C')], 'Center Only Toggle', 'Press ${triggers}'),
|
||||
};
|
||||
const MesoFocusLociParams = {
|
||||
|
||||
export const MesoFocusLociParams = {
|
||||
minRadius: PD.Numeric(8, { min: 1, max: 50, step: 1 }),
|
||||
extraRadius: PD.Numeric(4, { min: 1, max: 50, step: 1 }, { description: 'Value added to the bounding-sphere radius of the Loci' }),
|
||||
durationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'Camera transition duration' }),
|
||||
centerOnly: PD.Boolean(true, { description: 'Keep current camera distance' }),
|
||||
|
||||
bindings: PD.Value(DefaultMesoFocusLociBindings, { isHidden: true }),
|
||||
};
|
||||
type MesoFocusLociProps = PD.Values<typeof MesoFocusLociParams>
|
||||
@@ -47,25 +50,45 @@ export const MesoFocusLoci = PluginBehavior.create<MesoFocusLociProps>({
|
||||
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
|
||||
|
||||
const { clickCenter, clickCenterFocus } = this.params.bindings;
|
||||
const { durationMs, extraRadius, minRadius } = this.params;
|
||||
const { durationMs, extraRadius, minRadius, centerOnly } = this.params;
|
||||
const radius = Math.max(sphere.radius + extraRadius, minRadius);
|
||||
|
||||
if (Binding.match(clickCenter, button, modifiers)) {
|
||||
// left mouse button
|
||||
if (Loci.isEmpty(current.loci)) {
|
||||
PluginCommands.Camera.Reset(this.ctx, { });
|
||||
return;
|
||||
}
|
||||
|
||||
const snapshot = canvas3d.camera.getCenter(sphere.center);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
if (centerOnly) {
|
||||
const snapshot = canvas3d.camera.getCenter(sphere.center);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
} else {
|
||||
this.ctx.managers.camera.focusSphere(sphere, this.params);
|
||||
}
|
||||
}
|
||||
} else if (Binding.match(clickCenterFocus, button, modifiers)) {
|
||||
// right mouse button
|
||||
if (Loci.isEmpty(current.loci)) {
|
||||
PluginCommands.Camera.Reset(this.ctx, { });
|
||||
return;
|
||||
}
|
||||
if (centerOnly) {
|
||||
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
} else {
|
||||
this.ctx.managers.camera.focusSphere(sphere, this.params);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const snapshot = canvas3d.camera.getCenter(sphere.center, radius);
|
||||
canvas3d.requestCameraReset({ durationMs, snapshot });
|
||||
this.subscribeObservable(this.ctx.behaviors.interaction.key, ({ code, key, modifiers }) => {
|
||||
if (!this.ctx.canvas3d) return;
|
||||
const b = { ...DefaultMesoFocusLociBindings, ...this.params.bindings };
|
||||
const { centerOnly } = this.params;
|
||||
|
||||
if (Binding.matchKey(b.keyCenterOnly, code, modifiers, key)) {
|
||||
this.params.centerOnly = !centerOnly;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,12 +16,16 @@ import { StateTreeSpine } from '../../../mol-state/tree/spine';
|
||||
import { Representation } from '../../../mol-repr/representation';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { MesoscaleState, expandAllGroups, getCellDescription, getEveryEntity } from '../data/state';
|
||||
|
||||
const B = ButtonsType;
|
||||
const M = ModifiersKeys;
|
||||
const Trigger = Binding.Trigger;
|
||||
|
||||
const DefaultMesoSelectLociBindings = {
|
||||
click: Binding([
|
||||
Trigger(B.Flag.Primary, M.create())
|
||||
], 'Click', 'Click element using ${triggers}'),
|
||||
clickToggleSelect: Binding([
|
||||
Trigger(B.Flag.Primary, M.create({ shift: true })),
|
||||
Trigger(B.Flag.Primary, M.create({ control: true })),
|
||||
@@ -63,15 +67,36 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
this.subscribeObservable(this.ctx.behaviors.interaction.click, ({ current, button, modifiers }) => {
|
||||
if (!this.ctx.canvas3d || this.ctx.isBusy) return;
|
||||
|
||||
const { clickToggleSelect } = this.params.bindings;
|
||||
const { click, clickToggleSelect } = this.params.bindings;
|
||||
if (Binding.match(clickToggleSelect, button, modifiers)) {
|
||||
if (Loci.isEmpty(current.loci)) {
|
||||
this.ctx.managers.interactivity.lociSelects.deselectAll();
|
||||
return;
|
||||
}
|
||||
|
||||
const loci = Loci.normalize(current.loci, modifiers.control ? 'entity' : 'chain');
|
||||
this.ctx.managers.interactivity.lociSelects.toggle({ loci }, false);
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
|
||||
const d = getCellDescription(cell!);
|
||||
MesoscaleState.set(this.ctx, { focusInfo: `${d}` });
|
||||
}
|
||||
}
|
||||
if (Binding.match(click, button, modifiers)) {
|
||||
if (Loci.isEmpty(current.loci)) {
|
||||
MesoscaleState.set(this.ctx, { focusInfo: '', filter: '' });
|
||||
return;
|
||||
}
|
||||
const snapshotKey = current.repr?.props?.snapshotKey?.trim() ?? '';
|
||||
if (snapshotKey) {
|
||||
this.ctx.managers.snapshot.applyKey(snapshotKey);
|
||||
} else {
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
|
||||
const d = getCellDescription(cell!);
|
||||
MesoscaleState.set(this.ctx, { focusInfo: `${d}`, filter: `${cell?.obj?.label}` });
|
||||
expandAllGroups(this.ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
this.ctx.managers.interactivity.lociSelects.addProvider(this.lociMarkProvider);
|
||||
@@ -87,22 +112,41 @@ export const MesoSelectLoci = PluginBehavior.create<MesoSelectLociProps>({
|
||||
this.ctx.managers.interactivity.lociHighlights.clearHighlights();
|
||||
return;
|
||||
}
|
||||
|
||||
if (modifiers.control) {
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
|
||||
} else {
|
||||
const loci = Loci.normalize(current.loci, 'chain');
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
if (modifiers.control) {
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci: EveryLoci }, false);
|
||||
} else {
|
||||
const loci = Loci.normalize(current.loci, 'chain');
|
||||
this.ctx.managers.interactivity.lociHighlights.highlightOnly({ repr: current.repr, loci }, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Loci.isEmpty(current.loci)) {
|
||||
this.ctx.behaviors.labels.highlight.next({ labels: [] });
|
||||
this.ctx.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
} else {
|
||||
const labels: string[] = [];
|
||||
if (StructureElement.Loci.is(current.loci)) {
|
||||
const cell = this.ctx.helpers.substructureParent.get(current.loci.structure);
|
||||
labels.push(cell?.obj?.label || 'Unknown');
|
||||
const d = getCellDescription(cell!);
|
||||
labels.push(d);
|
||||
} else {
|
||||
const loci = Loci.normalize(current.loci, this.ctx.managers.interactivity.props.granularity);
|
||||
if (loci.kind === 'group-loci') {
|
||||
if ('shape' in current.loci && current.loci.shape.geometry.kind === 'text') {
|
||||
const qname = current.repr?.props.customText;
|
||||
// highlight protein with same name
|
||||
const entities = getEveryEntity(this.ctx, qname);
|
||||
for (const r of entities) {
|
||||
const repr = r.obj?.data.repr;
|
||||
if (repr) {
|
||||
this.ctx.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
labels.push(loci.shape.getLabel(0, 0));
|
||||
}
|
||||
}
|
||||
this.ctx.behaviors.labels.highlight.next({ labels });
|
||||
}
|
||||
|
||||
@@ -161,15 +161,12 @@ const CellpackStructure = PluginStateTransform.BuiltIn({
|
||||
|
||||
const unitsByEntity = getUnitsByEntity(parent);
|
||||
const units = unitsByEntity.get(idx) || [];
|
||||
// if (!unitsByEntity.get(idx)) {
|
||||
// console.log(entities.data.pdbx_description.value(idx));
|
||||
// }
|
||||
|
||||
const structure = Structure.create(units);
|
||||
|
||||
const description = entities.data.pdbx_description.value(idx)[0] || 'model';
|
||||
const label = description.split('.').at(-1) || a.label;
|
||||
|
||||
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
|
||||
const description_label = entities.data.pdbx_description.value(idx)[0] || 'model';
|
||||
const label = description_label.split('.').at(-1) || a.label;
|
||||
const description = entities.data.pdbx_parent_entity_id.value(idx) || label;
|
||||
return new PSO.Molecule.Structure(structure, { label, description: description }); // `${a.description}`
|
||||
});
|
||||
},
|
||||
dispose({ b }) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
|
||||
import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../mol-state';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { CellpackAssembly, CellpackStructure } from './model';
|
||||
|
||||
function getSpacefillParams(color: Color, sizeFactor: number, graphics: GraphicsMode, merge?: boolean) {
|
||||
@@ -96,12 +96,12 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
|
||||
const compRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged('group:comp:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `comp:`, label: 'compartment', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:comp:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const funcRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged('group:func:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `func:`, label: 'function', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:func:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
if (entities._rowCount > 1) {
|
||||
@@ -159,7 +159,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
parent.cell!.state.isCollapsed = false;
|
||||
const group = await state.build()
|
||||
.to(parent)
|
||||
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged(`group:comp:${n}`, MesoscaleGroup, { ...groupParams, root: parent === compRoot, index: colorIdx, tag: `comp:${n}`, label, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `comp:${p}`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
compGroups.set(n, group);
|
||||
}
|
||||
@@ -171,7 +171,7 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
const color = colorIdx !== undefined ? baseFuncColors[colorIdx] : ColorNames.white;
|
||||
const group = await state.build()
|
||||
.to(funcRoot)
|
||||
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged(`group:func:${f}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `func:${f}`, label: f, color: { type: 'custom', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'func:', state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
funcGroups.set(f, group);
|
||||
}
|
||||
@@ -201,6 +201,9 @@ export async function createCellpackHierarchy(plugin: PluginContext, trajectory:
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode), { tags: [`comp:${n}`, `func:${f}`] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -76,6 +76,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
|
||||
params: {
|
||||
instances: PD.Value<GenericInstances<Asset>>(EmptyInstances),
|
||||
label: PD.Optional(PD.Text('')),
|
||||
description: PD.Optional(PD.Text('')),
|
||||
cellSize: PD.Numeric(500, { min: 0, max: 10000, step: 100 }),
|
||||
}
|
||||
})({
|
||||
@@ -111,7 +112,7 @@ const StructureFromGeneric = PluginStateTransform.BuiltIn({
|
||||
structure = assembler.getStructure();
|
||||
}
|
||||
|
||||
const props = { label, description: Structure.elementDescription(structure) };
|
||||
const props = { label, description: params.description || Structure.elementDescription(structure) };
|
||||
return new SO.Molecule.Structure(structure, props);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -124,7 +124,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
async function addGroup(g: GenericGroup, cell: StateObjectSelector, parent: string) {
|
||||
const group = await state.build()
|
||||
.to(cell)
|
||||
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, index: undefined, tag: `${g.root}:${g.id}`, label: g.label || g.id, description: g.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: [`group:${g.root}:${g.id}`, g.root === parent ? `${g.root}:` : `${g.root}:${parent}`], state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
if (g.children) {
|
||||
@@ -137,7 +137,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
for (const r of manifest.roots) {
|
||||
const root = await state.build()
|
||||
.toRoot()
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.apply(MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `${r.id}:`, label: r.label || r.id, description: r.description, color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `group:${r.id}:`, state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
if (r.children) {
|
||||
@@ -193,11 +193,12 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
const file = Asset.File(new File([t], ent.file));
|
||||
|
||||
const color = ColorNames.skyblue;
|
||||
const label = ent.label || ent.file.split('.')[0];
|
||||
|
||||
const sizeFactor = ent.sizeFactor || 1;
|
||||
const tags = ent.groups.map(({ id, root }) => `${root}:${id}`);
|
||||
const instances = ent.instances && getAssetInstances(ent.instances);
|
||||
|
||||
const description = ent.description;
|
||||
const label = ent.label || ent.file.split('.')[0];
|
||||
build = build
|
||||
.toRoot()
|
||||
.apply(ReadFile, { file, label, isBinary });
|
||||
@@ -233,7 +234,7 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
|
||||
build = build
|
||||
.apply(ModelFromTrajectory, { modelIndex: 0 })
|
||||
.apply(StructureFromGeneric, { instances, label })
|
||||
.apply(StructureFromGeneric, { instances, label, description })
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(color, sizeFactor, graphicsMode, clipVariant), { tags });
|
||||
} else if (['ply'].includes(info.ext)) {
|
||||
if (['ply'].includes(info.ext)) {
|
||||
@@ -250,9 +251,9 @@ export async function createGenericHierarchy(plugin: PluginContext, file: Asset.
|
||||
}
|
||||
await build.commit();
|
||||
|
||||
const rootId = `${manifest.roots[0].id}:`;
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
await updateColors(plugin, values, rootId, '');
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -186,9 +186,10 @@ const MmcifStructure = PluginStateTransform.BuiltIn({
|
||||
} else {
|
||||
structure = Structure.create(units);
|
||||
}
|
||||
|
||||
// could also use _struct_ref.pdbx_db_accession to point to uniprot with _struct_ref.db_name == UNP
|
||||
const label = entities.data.pdbx_description.value(idx).join(', ') || 'model';
|
||||
return new PSO.Molecule.Structure(structure, { label, description: `${a.description}` });
|
||||
const description = `*Entity id* ${entities.data.id.value(idx)} *src_method* ${entities.data.src_method.value(idx)} *type* ${entities.data.type.value(idx)}`;
|
||||
return new PSO.Molecule.Structure(structure, { label, description: description });
|
||||
});
|
||||
},
|
||||
update({ newParams, oldParams }) {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { StateObjectRef, StateObjectSelector, StateBuilder } from '../../../../m
|
||||
import { Clip } from '../../../../mol-util/clip';
|
||||
import { Color } from '../../../../mol-util/color';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getDistinctGroupColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { MmcifAssembly, MmcifStructure } from './model';
|
||||
|
||||
function getSpacefillParams(color: Color, scaleFactor: number, graphics: GraphicsMode, clipVariant: Clip.Variant) {
|
||||
@@ -114,7 +114,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
|
||||
const entRoot = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'custom', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const getEntityType = (i: number) => {
|
||||
@@ -148,7 +148,7 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
const color = colorIdx !== undefined ? baseEntColors[colorIdx] : ColorNames.white;
|
||||
const group = await state.build()
|
||||
.to(entRoot)
|
||||
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged(`group:ent:${t}`, MesoscaleGroup, { ...groupParams, index: colorIdx, tag: `ent:${t}`, label: t, color: { type: 'generate', illustrative: false, value: color, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
entGroups.set(t, group);
|
||||
}
|
||||
@@ -172,6 +172,9 @@ export async function createMmcifHierarchy(plugin: PluginContext, trajectory: St
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(color, scaleFactor, graphicsMode, clipVariant), { tags: [`ent:${t}`] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -39,8 +39,10 @@ const StructureFromPetworld = PluginStateTransform.BuiltIn({
|
||||
|
||||
const { frame } = s.model.sourceData.data;
|
||||
const pdbx_model = frame.categories.pdbx_model.getField('name')!;
|
||||
const pdbx_description = frame.categories.pdbx_model.getField('description')!;
|
||||
const description = pdbx_description ? pdbx_description.str(params.modelIndex) : Structure.elementDescription(s);
|
||||
const label = pdbx_model.str(params.modelIndex);
|
||||
const props = { label, description: Structure.elementDescription(s) };
|
||||
const props = { label, description: description };
|
||||
return new SO.Molecule.Structure(s, props);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ import { SpacefillRepresentationProvider } from '../../../../mol-repr/structure/
|
||||
import { StructureRepresentation3D } from '../../../../mol-plugin-state/transforms/representation';
|
||||
import { PluginContext } from '../../../../mol-plugin/context';
|
||||
import { PluginStateObject } from '../../../../mol-plugin-state/objects';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams } from '../state';
|
||||
import { GraphicsMode, MesoscaleGroup, MesoscaleState, getDistinctBaseColors, getGraphicsModeProps, getMesoscaleGroupParams, updateColors } from '../state';
|
||||
import { ColorNames } from '../../../../mol-util/color/names';
|
||||
import { MmcifFormat } from '../../../../mol-model-formats/structure/mmcif';
|
||||
import { Task } from '../../../../mol-task';
|
||||
@@ -97,12 +97,12 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
|
||||
|
||||
const group = await state.build()
|
||||
.toRoot()
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged('group:ent:', MesoscaleGroup, { ...groupParams, root: true, index: -1, tag: `ent:`, label: 'entity', color: { type: 'generate', illustrative: false, value: ColorNames.white, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: 'group:ent:', state: { isCollapsed: false, isHidden: groupParams.hidden } })
|
||||
.commit({ revertOnError: true });
|
||||
|
||||
await state.build()
|
||||
.to(group)
|
||||
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.applyOrUpdateTagged(`group:ent:mem`, MesoscaleGroup, { ...groupParams, index: undefined, tag: `ent:mem`, label: 'Membrane', color: { type: 'uniform', illustrative: false, value: ColorNames.lightgrey, variability: 20, shift: 0, lightness: 0, alpha: 1, emissive: 0 } }, { tags: `ent:`, state: { isCollapsed: true, isHidden: groupParams.hidden } })
|
||||
.commit();
|
||||
|
||||
const colors = getDistinctBaseColors(other.length, 0);
|
||||
@@ -124,6 +124,9 @@ export async function createPetworldHierarchy(plugin: PluginContext, trajectory:
|
||||
.apply(StructureRepresentation3D, getSpacefillParams(colors[i], graphicsMode), { tags: [`ent:`] });
|
||||
}
|
||||
await build.commit();
|
||||
const values = { type: 'group-generate', value: ColorNames.white, lightness: 0, alpha: 1 };
|
||||
const options = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 }, celShaded: true, };
|
||||
await updateColors(plugin, values, options);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
plugin.log.error(e);
|
||||
|
||||
@@ -24,6 +24,7 @@ import { SpacefillRepresentationProvider } from '../../../mol-repr/structure/rep
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { saturate } from '../../../mol-math/interpolate';
|
||||
import { Material } from '../../../mol-util/material';
|
||||
|
||||
function getHueRange(hue: number, variability: number) {
|
||||
let min = hue - variability;
|
||||
@@ -106,6 +107,7 @@ export function getDistinctBaseColors(count: number, shift: number, props?: Part
|
||||
|
||||
export const ColorParams = {
|
||||
type: PD.Select('generate', PD.arrayToOptions(['generate', 'uniform', 'custom'])),
|
||||
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
|
||||
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type === 'custom' }),
|
||||
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'generate' }),
|
||||
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
|
||||
@@ -119,6 +121,7 @@ export const ColorValueParam = PD.Color(Color(0xFFFFFF));
|
||||
|
||||
export const RootParams = {
|
||||
type: PD.Select('custom', PD.arrayToOptions(['group-generate', 'group-uniform', 'generate', 'uniform', 'custom'])),
|
||||
illustrative: PD.Boolean(false, { description: 'Illustrative style', hideIf: p => p.type === 'custom' }),
|
||||
value: PD.Color(Color(0xFFFFFF), { hideIf: p => p.type !== 'uniform' }),
|
||||
variability: PD.Numeric(20, { min: 1, max: 180, step: 1 }, { hideIf: p => p.type !== 'group-generate' }),
|
||||
shift: PD.Numeric(0, { min: 0, max: 100, step: 1 }, { hideIf: p => !p.type.includes('generate') }),
|
||||
@@ -132,6 +135,10 @@ export const LightnessParams = {
|
||||
};
|
||||
export const DimLightness = 6;
|
||||
|
||||
export const IllustrativeParams = {
|
||||
illustrative: PD.Boolean(false, { description: 'Illustrative style' }),
|
||||
};
|
||||
|
||||
export const OpacityParams = {
|
||||
alpha: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
};
|
||||
@@ -140,11 +147,24 @@ export const EmissiveParams = {
|
||||
emissive: PD.Numeric(0, { min: 0, max: 1, step: 0.01 }),
|
||||
};
|
||||
|
||||
export const celShaded = {
|
||||
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' })
|
||||
};
|
||||
|
||||
export type celShadedProps = PD.Values<typeof celShaded>;
|
||||
|
||||
|
||||
export const PatternParams = {
|
||||
frequency: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
amplitude: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
|
||||
};
|
||||
|
||||
export const StyleParams = {
|
||||
ignoreLight: PD.Boolean(false, { description: 'Ignore light for stylized rendering of representations' }),
|
||||
materialStyle: Material.getParam(),
|
||||
celShaded: PD.Boolean(false, { description: 'Cel Shading light for stylized rendering of representations' }),
|
||||
};
|
||||
|
||||
export const LodParams = {
|
||||
lodLevels: Spheres.Params.lodLevels,
|
||||
cellSize: Spheres.Params.cellSize,
|
||||
@@ -250,6 +270,7 @@ export const MesoscaleGroupParams = {
|
||||
index: PD.Value<number>(-1, { isHidden: true }),
|
||||
tag: PD.Value<string>('', { isHidden: true }),
|
||||
label: PD.Value<string>('', { isHidden: true }),
|
||||
description: PD.Value<string>('', { isHidden: true }),
|
||||
hidden: PD.Boolean(false),
|
||||
color: PD.Group(RootParams),
|
||||
lightness: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
|
||||
@@ -271,7 +292,7 @@ export const MesoscaleGroup = PluginStateTransform.BuiltIn({
|
||||
})({
|
||||
apply({ a, params }, plugin: PluginContext) {
|
||||
return Task.create('Apply Mesoscale Group', async () => {
|
||||
return new MesoscaleGroupObject({}, { label: params.label });
|
||||
return new MesoscaleGroupObject({}, { label: params.label, description: params.description });
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -360,7 +381,10 @@ export const MesoscaleStateParams = {
|
||||
filter: PD.Value<string>('', { isHidden: true }),
|
||||
graphics: PD.Select('quality', PD.arrayToOptions(['ultra', 'quality', 'balanced', 'performance', 'custom'] as GraphicsMode[])),
|
||||
description: PD.Value<string>('', { isHidden: true }),
|
||||
focusInfo: PD.Value<string>('', { isHidden: true }),
|
||||
link: PD.Value<string>('', { isHidden: true }),
|
||||
textSizeDescription: PD.Numeric(14, { min: 1, max: 100, step: 1 }, { isHidden: true }),
|
||||
index: PD.Value<number>(-1, { isHidden: true })
|
||||
};
|
||||
|
||||
export class MesoscaleStateObject extends PSO.Create<MesoscaleState>({ name: 'Mesoscale State', typeClass: 'Object' }) { }
|
||||
@@ -447,7 +471,7 @@ export function getAllGroups(plugin: PluginContext, tag?: string) {
|
||||
return _getAllGroups(plugin, tag, []);
|
||||
}
|
||||
|
||||
export function getAllLeafGroups(plugin: PluginContext, tag: string) {
|
||||
export function getAllLeafGroups(plugin: PluginContext, tag: string | undefined) {
|
||||
const allGroups = getAllGroups(plugin, tag);
|
||||
allGroups.sort((a, b) => a.params?.values.index - b.params?.values.index);
|
||||
return allGroups.filter(g => {
|
||||
@@ -481,7 +505,8 @@ function getFilterMatcher(filter: string) {
|
||||
: new RegExp(escapeRegExp(filter), 'gi');
|
||||
}
|
||||
|
||||
export function getFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
|
||||
export function getFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
|
||||
if (!filter) return getEntities(plugin, tag);
|
||||
const matcher = getFilterMatcher(filter);
|
||||
return getEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
|
||||
}
|
||||
@@ -498,21 +523,42 @@ export function getAllEntities(plugin: PluginContext, tag?: string) {
|
||||
return _getAllEntities(plugin, tag, []);
|
||||
}
|
||||
|
||||
export function getAllFilteredEntities(plugin: PluginContext, tag: string, filter: string) {
|
||||
export function getAllFilteredEntities(plugin: PluginContext, tag: string | undefined, filter: string | undefined) {
|
||||
if (!filter) return getAllEntities(plugin, tag);
|
||||
const matcher = getFilterMatcher(filter);
|
||||
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
|
||||
}
|
||||
|
||||
export function getEveryEntity(plugin: PluginContext, filter?: string, tag?: string) {
|
||||
if (filter) {
|
||||
const matcher = getFilterMatcher(filter);
|
||||
return getAllEntities(plugin, tag).filter(c => getEntityLabel(plugin, c).match(matcher) !== null);
|
||||
} else {
|
||||
return getAllEntities(plugin, tag);
|
||||
}
|
||||
}
|
||||
|
||||
export function getEntityLabel(plugin: PluginContext, cell: StateObjectCell) {
|
||||
return StateObjectRef.resolve(plugin.state.data, cell.transform.parent)?.obj?.label || 'Entity';
|
||||
}
|
||||
|
||||
//
|
||||
export function getCellDescription(cell: StateObjectCell) {
|
||||
// markdown style for description
|
||||
return '**' + cell?.obj?.label + '**\n\n' + cell?.obj?.description;
|
||||
}
|
||||
|
||||
export async function updateColors(plugin: PluginContext, values: PD.Values, tag: string, filter: string) {
|
||||
export function getEntityDescription(plugin: PluginContext, cell: StateObjectCell) {
|
||||
const s = StateObjectRef.resolve(plugin.state.data, cell.transform.parent);
|
||||
const d = getCellDescription(s!);
|
||||
return d;
|
||||
}
|
||||
|
||||
|
||||
export async function updateColors(plugin: PluginContext, values: PD.Values, options?: PD.Values, tag?: string, filter?: string) {
|
||||
const update = plugin.state.data.build();
|
||||
const { type, value, shift, lightness, alpha, emissive } = values;
|
||||
|
||||
const { type, illustrative, value, shift, lightness, alpha, emissive } = values;
|
||||
const doLighting = (options !== undefined);
|
||||
const { ignoreLight, materialStyle: material, celShaded } = options ? options : { ignoreLight: true, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 }, celShaded: false };
|
||||
if (type === 'group-generate' || type === 'group-uniform') {
|
||||
const groups = getAllLeafGroups(plugin, tag);
|
||||
const baseColors = getDistinctBaseColors(groups.length, shift);
|
||||
@@ -531,11 +577,19 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
|
||||
const c = type === 'group-generate' ? groupColors[j] : baseColors[i];
|
||||
update.to(entities[j]).update(old => {
|
||||
if (old.type) {
|
||||
old.colorTheme.params.value = c;
|
||||
old.colorTheme.params.lightness = lightness;
|
||||
if (illustrative) {
|
||||
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
|
||||
} else {
|
||||
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
|
||||
}
|
||||
old.type.params.alpha = alpha;
|
||||
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
|
||||
old.type.params.emissive = emissive;
|
||||
if (doLighting) {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = c;
|
||||
old.coloring.params.lightness = lightness;
|
||||
@@ -548,6 +602,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
|
||||
|
||||
update.to(g.transform.ref).update(old => {
|
||||
old.color.type = type === 'group-generate' ? 'generate' : 'uniform';
|
||||
old.color.illustrative = illustrative;
|
||||
old.color.value = baseColors[i];
|
||||
old.color.lightness = lightness;
|
||||
old.color.alpha = alpha;
|
||||
@@ -566,11 +621,19 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
|
||||
const c = type === 'generate' ? groupColors[j] : value;
|
||||
update.to(entities[j]).update(old => {
|
||||
if (old.type) {
|
||||
old.colorTheme.params.value = c;
|
||||
old.colorTheme.params.lightness = lightness;
|
||||
if (illustrative) {
|
||||
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness: lightness } } } };
|
||||
} else {
|
||||
old.colorTheme = { name: 'uniform', params: { value: c, lightness: lightness } };
|
||||
}
|
||||
old.type.params.alpha = alpha;
|
||||
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
|
||||
old.type.params.emissive = emissive;
|
||||
if (doLighting) {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = c;
|
||||
old.coloring.params.lightness = lightness;
|
||||
@@ -585,6 +648,7 @@ export async function updateColors(plugin: PluginContext, values: PD.Values, tag
|
||||
for (const o of others) {
|
||||
update.to(o).update(old => {
|
||||
old.color.type = type === 'generate' ? 'custom' : 'uniform';
|
||||
old.color.illustrative = illustrative;
|
||||
old.color.value = value;
|
||||
old.color.lightness = lightness;
|
||||
old.color.alpha = alpha;
|
||||
@@ -603,3 +667,17 @@ export function expandAllGroups(plugin: PluginContext) {
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateReprParams(plugin: PluginContext, options: PD.Values) {
|
||||
const update = plugin.state.data.build();
|
||||
const { ignoreLight, materialStyle: material, celShaded } = options;
|
||||
const entities = getAllEntities(plugin);
|
||||
for (let j = 0; j < entities.length; ++j) {
|
||||
update.to(entities[j]).update(old => {
|
||||
old.type.params.ignoreLight = ignoreLight;
|
||||
old.type.params.material = material;
|
||||
old.type.params.celShaded = celShaded;
|
||||
});
|
||||
}
|
||||
await update.commit();
|
||||
}
|
||||
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,6 +4,7 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
|
||||
<link rel="icon" href="./favicon.ico" type="image/x-icon">
|
||||
<link rel="stylesheet" href="../extras/driver.css"/>
|
||||
<title>Mol* Mesoscale Explorer</title>
|
||||
<style>
|
||||
* {
|
||||
@@ -38,8 +39,11 @@
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="../extras/driver.js.iife.js"></script>
|
||||
<script type="text/javascript" src="./molstar.js"></script>
|
||||
<script type="text/javascript">
|
||||
const driver = window.driver ? window.driver.js.driver() : undefined;
|
||||
|
||||
function getParam(name, regex) {
|
||||
var r = new RegExp(name + '=' + '(' + regex + ')[&]?', 'i');
|
||||
return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
|
||||
@@ -64,6 +68,7 @@
|
||||
allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
|
||||
powerPreference: powerPreference || 'high-performance',
|
||||
graphicsMode: graphicsMode || 'quality',
|
||||
driver: driver
|
||||
}).then(me => {
|
||||
var example = getParam('example', '[^&]+').trim();
|
||||
if (example) {
|
||||
@@ -89,7 +94,8 @@
|
||||
me.loadPdbDev(pdbdev);
|
||||
return;
|
||||
}
|
||||
|
||||
// load a default tour if no param provided
|
||||
me.loadExample('machineryoflife-tour');
|
||||
window.addEventListener('unload', () => {
|
||||
// to aid GC
|
||||
me.dispose();
|
||||
@@ -98,4 +104,4 @@
|
||||
</script>
|
||||
<!-- __MOLSTAR_ANALYTICS__ -->
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -24,6 +24,7 @@ $logo-background: rgba(0,0,0,0.75);
|
||||
}
|
||||
|
||||
@import 'mol-plugin-ui/skin/base/base';
|
||||
@import 'mol-plugin-ui/skin/base/variables';
|
||||
|
||||
a {
|
||||
color: $font-color;
|
||||
@@ -31,3 +32,35 @@ a {
|
||||
color: $hover-font-color;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.msp-snapshot-description-me {
|
||||
background: rgba(red($default-background), green($default-background), blue($default-background), 0.5);
|
||||
|
||||
position: absolute;
|
||||
height: 50vh; // 50% of the viewport height
|
||||
left: 0;
|
||||
top: $control-spacing + $row-height;
|
||||
padding: (0.66 * $control-spacing) $control-spacing;
|
||||
|
||||
resize: both; /* Allows resizing in both directions */
|
||||
overflow: auto; /* Adjust as needed */
|
||||
|
||||
a {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: $font-color;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
padding-left: $control-spacing + 4px;
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.shown {
|
||||
display: block; // or 'flex', 'grid', etc. depending on your layout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { PluginReactContext, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { Button, ControlGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { ArrowDropDownSvg, ArrowRightSvg, CloseSvg, VisibilityOffOutlinedSvg, VisibilityOutlinedSvg, ContentCutSvg, BrushSvg, SearchSvg, TooltipTextSvg, TooltipTextOutlineSvg, PlusBoxSvg, MinusBoxSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { State, StateObjectCell, StateSelection, StateTransformer } from '../../../mol-state';
|
||||
import { ParameterControls, ParameterMappingControl, ParamOnChange, SelectControl } from '../../../mol-plugin-ui/controls/parameters';
|
||||
@@ -18,14 +18,17 @@ import { CombinedColorControl } from '../../../mol-plugin-ui/controls/color';
|
||||
import { MarkerAction } from '../../../mol-util/marker-action';
|
||||
import { EveryLoci, Loci } from '../../../mol-model/loci';
|
||||
import { deepEqual } from '../../../mol-util';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams } from '../data/state';
|
||||
import React from 'react';
|
||||
import { ColorValueParam, ColorParams, ColorProps, DimLightness, LightnessParams, LodParams, MesoscaleGroup, MesoscaleGroupProps, OpacityParams, SimpleClipParams, SimpleClipProps, createClipMapping, getClipObjects, getDistinctGroupColors, RootParams, MesoscaleState, getRoots, getAllGroups, getAllLeafGroups, getFilteredEntities, getAllFilteredEntities, getGroups, getEntities, getAllEntities, getEntityLabel, updateColors, getGraphicsModeProps, GraphicsMode, MesoscaleStateParams, setGraphicsCanvas3DProps, PatternParams, expandAllGroups, EmissiveParams, IllustrativeParams, getCellDescription, getEntityDescription, getEveryEntity } from '../data/state';
|
||||
import React, { useState } from 'react';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { StructureElement } from '../../../mol-model/structure/structure/element';
|
||||
import { PluginStateObject as PSO } from '../../../mol-plugin-state/objects';
|
||||
import { Structure } from '../../../mol-model/structure';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { MesoFocusLoci } from '../behavior/camera';
|
||||
import Markdown from 'react-markdown';
|
||||
import { combineLatest } from 'rxjs';
|
||||
|
||||
function centerLoci(plugin: PluginContext, loci: Loci, durationMs = 250) {
|
||||
const { canvas3d } = plugin;
|
||||
@@ -60,6 +63,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
if (!state.description && !state.link) return;
|
||||
|
||||
return {
|
||||
selectionDescription: state.focusInfo,
|
||||
description: state.description,
|
||||
link: state.link,
|
||||
};
|
||||
@@ -68,7 +72,7 @@ export class ModelInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
render() {
|
||||
const info = this.info;
|
||||
return info && <>
|
||||
<div className='msp-help-text'>
|
||||
<div id='modelinfo' className='msp-help-text'>
|
||||
<div>{info.description}</div>
|
||||
<div><a href={info.link} target='_blank'>Source</a></div>
|
||||
</div>
|
||||
@@ -101,17 +105,20 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
|
||||
}
|
||||
|
||||
get info() {
|
||||
const info: { label: string, key: string }[] = [];
|
||||
const infos: { label: string, key: string, description?: string }[] = [];
|
||||
this.plugin.managers.structure.selection.entries.forEach((e, k) => {
|
||||
if (StructureElement.Loci.is(e.selection) && !StructureElement.Loci.isEmpty(e.selection)) {
|
||||
const cell = this.plugin.helpers.substructureParent.get(e.selection.structure);
|
||||
info.push({
|
||||
const { entities } = e.selection.structure.model;
|
||||
const description = entities.data.pdbx_description.value(0)[0] || 'model';
|
||||
infos.push({
|
||||
description: description,
|
||||
label: cell?.obj?.label || 'Unknown',
|
||||
key: k,
|
||||
});
|
||||
}
|
||||
});
|
||||
return info;
|
||||
return infos;
|
||||
}
|
||||
|
||||
find(label: string) {
|
||||
@@ -133,31 +140,40 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
|
||||
|
||||
const loci = Structure.toStructureElementLoci(e.selection.structure);
|
||||
centerLoci(this.plugin, loci);
|
||||
const cell = this.plugin.helpers.substructureParent.get(loci.structure);
|
||||
const d = getCellDescription(cell!); // '### ' + cell?.obj?.label + '\n\n' + cell?.obj?.description;
|
||||
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
|
||||
}
|
||||
|
||||
get selection() {
|
||||
const info = this.info;
|
||||
const help_selection = <><div>Use <i>ctrl+left</i> to select entities, either on the 3D canvas or in the tree below</div><div>Use <i>shift+left</i> to select individual chain on the 3D canvas</div></>;
|
||||
if (!info.length) return <>
|
||||
<div className='msp-help-text'>
|
||||
<div>Use <i>ctrl+left click</i> to select entities, either on the 3D canvas or in the tree below</div>
|
||||
<div id='seleinfo' className='msp-help-text'>
|
||||
{help_selection}
|
||||
</div>
|
||||
</>;
|
||||
|
||||
return <>
|
||||
{info.map((entry, index) => {
|
||||
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
|
||||
onClick={() => this.center(entry.key)}
|
||||
>
|
||||
<span title={entry.label}>{entry.label}</span>
|
||||
</Button>;
|
||||
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
|
||||
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
|
||||
return <div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
|
||||
{label}
|
||||
{find}
|
||||
{remove}
|
||||
</div>;
|
||||
})}
|
||||
<div id='seleinfo'>
|
||||
{info.map((entry, index) => {
|
||||
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={this.state.isDisabled}
|
||||
onClick={() => this.center(entry.key)}
|
||||
>
|
||||
<span title={entry.label}>
|
||||
{entry.label}
|
||||
</span>
|
||||
</Button>;
|
||||
const find = <IconButton svg={SearchSvg} toggleState={false} disabled={this.state.isDisabled} small onClick={() => this.find(entry.label)} />;
|
||||
const remove = <IconButton svg={CloseSvg} toggleState={false} disabled={this.state.isDisabled} onClick={() => this.remove(entry.key)} />;
|
||||
return <>
|
||||
<div key={index} className={`msp-flex-row`} style={{ margin: `1px 5px 1px ${1 * 10 + 5}px` }}>
|
||||
{label}
|
||||
{find}
|
||||
{remove}
|
||||
</div>
|
||||
</>;
|
||||
})};
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
|
||||
@@ -216,7 +232,7 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
|
||||
|
||||
renderStyle() {
|
||||
const style = this.style || '';
|
||||
return <div style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
return <div id='selestyle' style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
<SelectControl name={'Style'} param={SelectionStyleParam} value={style} onChange={(e) => { this.setStyle(e.value); }} />
|
||||
</div>;
|
||||
}
|
||||
@@ -229,6 +245,192 @@ export class SelectionInfo extends PluginUIComponent<{}, { isDisabled: boolean }
|
||||
}
|
||||
}
|
||||
|
||||
export function MesoMarkdownAnchor({ href, children, element }: { href?: string, children?: any, element?: any }) {
|
||||
const plugin = React.useContext(PluginReactContext);
|
||||
if (!href) return element;
|
||||
// Decode the href to handle encoded spaces and other characters
|
||||
const decodedHref = href ? decodeURIComponent(href) : '';
|
||||
const handleHover = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (decodedHref.startsWith('i')) {
|
||||
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
const query_names = decodedHref.substring(1).split(',');
|
||||
for (const query_name of query_names) {
|
||||
const entities = getEveryEntity(plugin, query_name);
|
||||
for (const r of entities) {
|
||||
const repr = r.obj?.data.repr;
|
||||
if (repr) {
|
||||
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (decodedHref.startsWith('g')) {
|
||||
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
const qindex = decodedHref.indexOf('.');
|
||||
const query = decodedHref.substring(1, qindex) + ':';
|
||||
const query_names = decodedHref.substring(qindex + 1).split(',');
|
||||
for (const query_name of query_names) {
|
||||
const e = getAllEntities(plugin, query + query_name);
|
||||
for (const r of e) {
|
||||
const repr = r.obj?.data.repr;
|
||||
if (repr) {
|
||||
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
const handleLeave = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
// Implement your hover off logic here
|
||||
// Example: Perform an action if the href starts with 'h'
|
||||
if (decodedHref.startsWith('i') || decodedHref.startsWith('g')) {
|
||||
// Example hover off action
|
||||
e.preventDefault();
|
||||
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
}
|
||||
};
|
||||
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
if (href.startsWith('#')) {
|
||||
plugin.managers.snapshot.applyKey(decodedHref.substring(1));
|
||||
} else if (decodedHref.startsWith('i')) {
|
||||
plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
const query_names = decodedHref.substring(1).split(',');
|
||||
for (const query_name of query_names) {
|
||||
const entities = getFilteredEntities(plugin, '', query_name);
|
||||
for (const r of entities) {
|
||||
const repr = r.obj?.data.repr;
|
||||
if (repr) {
|
||||
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
|
||||
}
|
||||
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
|
||||
if (!(cell?.obj?.data.sourceData instanceof Structure)) {
|
||||
return;
|
||||
}
|
||||
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
|
||||
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
|
||||
}
|
||||
}
|
||||
} else if (decodedHref.startsWith('g')) {
|
||||
plugin.managers.interactivity.lociSelects.deselectAll();
|
||||
plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
const qindex = decodedHref.indexOf('.');
|
||||
const query = decodedHref.substring(1, qindex) + ':';
|
||||
const query_names = decodedHref.substring(qindex + 1).split(',');
|
||||
for (const query_name of query_names) {
|
||||
const entities = getAllEntities(plugin, query + query_name);
|
||||
for (const r of entities) {
|
||||
const repr = r.obj?.data.repr;
|
||||
if (repr) {
|
||||
plugin.canvas3d?.mark({ repr, loci: EveryLoci }, MarkerAction.Highlight);
|
||||
}
|
||||
const cell = r as StateObjectCell<PSO.Molecule.Structure.Representation3D | PSO.Shape.Representation3D> | undefined;
|
||||
if (!(cell?.obj?.data.sourceData instanceof Structure)) return;
|
||||
const loci = Structure.toStructureElementLoci(cell.obj.data.sourceData);
|
||||
plugin.managers.interactivity.lociSelects.toggle({ loci }, false);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// open the link in a new tab
|
||||
window.open(decodedHref, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
if (decodedHref[0] === '#') {
|
||||
return <a href={decodedHref[0]} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
|
||||
}
|
||||
if (decodedHref[0] === 'i' || decodedHref[0] === 'g') {
|
||||
return <a href={decodedHref[0]} onMouseLeave={handleLeave} onMouseOver={handleHover} onClick={handleClick}>{children}</a>;
|
||||
}
|
||||
if (decodedHref[0] === 'h') {
|
||||
return <a href={decodedHref[0]} onClick={handleClick} rel='noopener noreferrer'>{children}</a>;
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
export function MesoViewportSnapshotDescription() {
|
||||
let tSize = 14;
|
||||
const plugin = React.useContext(PluginReactContext);
|
||||
if (MesoscaleState.has(plugin)) {
|
||||
const state = MesoscaleState.get(plugin);
|
||||
tSize = state.textSizeDescription;
|
||||
}
|
||||
const [_, setV] = React.useState(0);
|
||||
const [isShown, setIsShown] = useState(true);
|
||||
const [textSize, setTextSize] = useState(tSize);
|
||||
const toggleVisibility = () => {
|
||||
setIsShown(!isShown);
|
||||
};
|
||||
|
||||
const increaseTextSize = () => {
|
||||
setTextSize(prevSize => Math.min(prevSize + 2, 50)); // Increase the text size by 2px, but not above 50px
|
||||
};
|
||||
|
||||
const decreaseTextSize = () => {
|
||||
setTextSize(prevSize => Math.max(prevSize - 2, 2)); // Decrease the text size by 2px, but not below 2px
|
||||
};
|
||||
|
||||
|
||||
React.useEffect(() => {
|
||||
const sub = plugin.managers.snapshot.events.changed.subscribe(() => setV(v => v + 1));
|
||||
return () => sub.unsubscribe();
|
||||
}, [plugin]);
|
||||
|
||||
const current = plugin.managers.snapshot.state.current;
|
||||
if (!current) return null;
|
||||
|
||||
const e = plugin.managers.snapshot.getEntry(current)!;
|
||||
if (!e?.description?.trim()) return null;
|
||||
if (MesoscaleState.has(plugin)) {
|
||||
MesoscaleState.set(plugin, { textSizeDescription: textSize });
|
||||
}
|
||||
const showInfo = <IconButton svg={isShown ? TooltipTextSvg : TooltipTextOutlineSvg} flex='20px' onClick={toggleVisibility} title={isShown ? 'Hide Description' : 'Show Description'}/>;
|
||||
const increasePoliceSize = <IconButton svg={PlusBoxSvg} flex='20px' onClick={increaseTextSize} title='Bigger Text' />;
|
||||
const decreasePoliceSize = <IconButton svg={MinusBoxSvg} flex='20px' onClick={decreaseTextSize} title='Smaller Text' />;
|
||||
return (
|
||||
<>
|
||||
<div id='snapinfoctrl' className="msp-state-snapshot-viewport-controls" style={{ marginRight: '30px' }}>
|
||||
{showInfo}{increasePoliceSize}{decreasePoliceSize}
|
||||
</div>
|
||||
<div id='snapinfo' className={`msp-snapshot-description-me ${isShown ? 'shown' : 'hidden'}`} style={{ fontSize: `${textSize}px` }}>
|
||||
{<Markdown skipHtml={false} components={{ a: MesoMarkdownAnchor }}>{e.description}</Markdown>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export class FocusInfo extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
componentDidMount() {
|
||||
this.subscribe(combineLatest([
|
||||
this.plugin.state.data.behaviors.isUpdating,
|
||||
this.plugin.managers.structure.selection.events.changed
|
||||
]), ([isUpdating]) => {
|
||||
if (!isUpdating) this.forceUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
get info() {
|
||||
let focusInfo = '';
|
||||
if (MesoscaleState.has(this.plugin)) {
|
||||
const state = MesoscaleState.get(this.plugin);
|
||||
if (state.focusInfo) focusInfo = state.focusInfo;
|
||||
}
|
||||
return focusInfo;
|
||||
}
|
||||
|
||||
render() {
|
||||
const focusInfo = this.info;
|
||||
const description = (focusInfo !== '') ? <Markdown skipHtml components={{ a: MesoMarkdownAnchor }}>{focusInfo}</Markdown> : '';
|
||||
return <>
|
||||
<div id='focusinfo' className='msp-help-text'>
|
||||
{description}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
filterRef = React.createRef<HTMLInputElement>();
|
||||
prevFilter = '';
|
||||
@@ -336,7 +538,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
|
||||
|
||||
renderGraphics() {
|
||||
const graphics = this.graphics;
|
||||
return <div style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
return <div id='graphicsquality' style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
<SelectControl name={'Graphics'} param={MesoscaleStateParams.graphics} value={`${graphics}`} onChange={(e) => { this.setGraphics(e.value); }} />
|
||||
</div>;
|
||||
}
|
||||
@@ -363,7 +565,7 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
|
||||
|
||||
return <>
|
||||
{this.renderGraphics()}
|
||||
<div className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
<div id='searchtree' className={`msp-flex-row msp-control-row`} style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
<input type='text' ref={this.filterRef}
|
||||
value={filter}
|
||||
placeholder='Search'
|
||||
@@ -373,10 +575,12 @@ export class EntityControls extends PluginUIComponent<{}, { isDisabled: boolean
|
||||
/>
|
||||
<IconButton svg={CloseSvg} toggleState={false} disabled={disabled} onClick={() => this.setFilter('')} />
|
||||
</div>
|
||||
{options.length > 1 && <div style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
{options.length > 1 && <div id='grouptree' style={{ margin: '5px', marginBottom: '10px' }}>
|
||||
<SelectControl name={'Group By'} param={groupParam} value={`${groupBy}`} onChange={(e) => { this.setGroupBy(parseInt(e.value)); }} />
|
||||
</div>}
|
||||
<GroupNode filter={filter} cell={root} depth={0} />
|
||||
<div id='tree' style={{ position: 'relative', overflowY: 'auto', borderBottom: '1px solid #000', maxHeight: '600px' }}>
|
||||
<GroupNode filter={filter} cell={root} depth={0} />
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
@@ -435,6 +639,12 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
this.setState({ action: this.state.action === 'root' ? undefined : 'root' });
|
||||
};
|
||||
|
||||
showInfo = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
const d = getCellDescription(this.cell); // '### ' + this.cell?.obj?.label + '\n\n' + this.cell?.obj?.description;
|
||||
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
|
||||
};
|
||||
|
||||
highlight = (e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
this.plugin.canvas3d?.mark({ loci: EveryLoci }, MarkerAction.RemoveHighlight);
|
||||
@@ -493,7 +703,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
|
||||
updateColor = (values: ColorProps) => {
|
||||
const update = this.plugin.state.data.build();
|
||||
const { value, type, lightness, alpha, emissive } = values;
|
||||
const { value, illustrative, type, lightness, alpha, emissive } = values;
|
||||
|
||||
const entities = this.filteredEntities;
|
||||
|
||||
@@ -507,8 +717,11 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const c = type === 'generate' ? groupColors[i] : value;
|
||||
update.to(entities[i]).update(old => {
|
||||
if (old.type) {
|
||||
old.colorTheme.params.value = c;
|
||||
old.colorTheme.params.lightness = lightness;
|
||||
if (illustrative) {
|
||||
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: c, lightness } } } };
|
||||
} else {
|
||||
old.colorTheme = { name: 'uniform', params: { value: c, lightness } };
|
||||
}
|
||||
old.type.params.alpha = alpha;
|
||||
old.type.params.xrayShaded = alpha < 1 ? 'inverted' : false;
|
||||
old.type.params.emissive = emissive;
|
||||
@@ -536,7 +749,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
};
|
||||
|
||||
updateRoot = async (values: PD.Values) => {
|
||||
await updateColors(this.plugin, values, this.cell.params?.values.tag, this.props.filter);
|
||||
await updateColors(this.plugin, values, undefined, this.cell.params?.values.tag, this.props.filter);
|
||||
|
||||
const update = this.plugin.state.data.build();
|
||||
|
||||
@@ -655,6 +868,7 @@ export class GroupNode extends Node<{ filter: string }, { isCollapsed: boolean,
|
||||
const label = <Button className={`msp-btn-tree-label`} noOverflow disabled={disabled}
|
||||
onMouseEnter={this.highlight}
|
||||
onMouseLeave={this.clearHighlight}
|
||||
onClick={this.showInfo}
|
||||
>
|
||||
<span title={groupLabel}>{groupLabel}</span>
|
||||
</Button>;
|
||||
@@ -774,18 +988,63 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
if (e.ctrlKey) {
|
||||
this.toggleSelect(e);
|
||||
} else {
|
||||
this.center(e);
|
||||
const d = getEntityDescription(this.plugin, this.cell);
|
||||
if (this.cell?.obj?.data.sourceData.state.models.length !== 0) {
|
||||
const repr = this.cell?.obj?.data.repr;
|
||||
if (repr) {
|
||||
// for fiber need to think how to handle.
|
||||
const aloci = repr.getAllLoci()[0];
|
||||
const locis = Loci.normalize(aloci, 'chainInstances') as StructureElement.Loci;
|
||||
const nChain = aloci.structure.state.unitSymmetryGroups.length;
|
||||
let index = MesoscaleState.get(this.plugin).index + 1;
|
||||
if (index * nChain >= locis.elements.length) index = 0;
|
||||
const elems = locis.elements.slice(index * nChain, ((index + 1) * nChain)); // end index is not included
|
||||
const loci = StructureElement.Loci(aloci.structure, elems);
|
||||
const sphere = Loci.getBoundingSphere(loci) || Sphere3D();
|
||||
const state = this.plugin.state.behaviors;
|
||||
const selections = state.select(StateSelection.Generators.ofTransformer(MesoFocusLoci));
|
||||
const params = selections.length === 1 ? selections[0].obj?.data.params : undefined;
|
||||
if (!params.centerOnly) {
|
||||
this.plugin.managers.camera.focusSphere(sphere, params);
|
||||
} else {
|
||||
const snapshot = this.plugin.canvas3d?.camera.getCenter(sphere.center);
|
||||
this.plugin.canvas3d?.requestCameraReset({ durationMs: params.durationMs, snapshot });
|
||||
}
|
||||
MesoscaleState.set(this.plugin, { index: index, focusInfo: `${d}` });
|
||||
}
|
||||
} else {
|
||||
this.center(e);
|
||||
MesoscaleState.set(this.plugin, { focusInfo: `${d}` });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
get colorValue(): Color | undefined {
|
||||
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
|
||||
if (this.cell.transform.params?.colorTheme?.params.value) {
|
||||
return this.cell.transform.params?.colorTheme?.params.value;
|
||||
} else if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
|
||||
return this.cell.transform.params?.colorTheme?.params.style.params.value;
|
||||
} else {
|
||||
return this.cell.transform.params?.colorTheme?.params.value ?? this.cell.transform.params?.coloring?.params.color;
|
||||
}
|
||||
}
|
||||
|
||||
get illustrativeValue(): { illustrative: boolean } | undefined {
|
||||
return {
|
||||
illustrative: (this.cell.transform.params?.colorTheme?.name === 'illustrative')
|
||||
};
|
||||
}
|
||||
|
||||
get lightnessValue(): { lightness: number } | undefined {
|
||||
return {
|
||||
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
|
||||
};
|
||||
if (this.cell.transform.params?.colorTheme?.name === 'illustrative') {
|
||||
return {
|
||||
lightness: this.cell.transform.params?.colorTheme?.params.style.params.lightness ?? 0
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
lightness: this.cell.transform.params?.colorTheme?.params.lightness ?? this.cell.transform.params?.coloring?.params.lightness ?? 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
get opacityValue(): { alpha: number } | undefined {
|
||||
@@ -838,7 +1097,11 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
}
|
||||
update.to(this.ref).update(old => {
|
||||
if (old.colorTheme) {
|
||||
old.colorTheme.params.value = value;
|
||||
if (old.colorTheme.name === 'illustrative') {
|
||||
old.colorTheme.params.style.params.value = value;
|
||||
} else {
|
||||
old.colorTheme.params.value = value;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.color = value;
|
||||
}
|
||||
@@ -846,10 +1109,26 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
update.commit();
|
||||
};
|
||||
|
||||
updateIllustrative = (values: PD.Values) => {
|
||||
return this.plugin.build().to(this.ref).update(old => {
|
||||
if (old.colorTheme) {
|
||||
if (old.colorTheme.name !== 'illustrative' && values.illustrative) {
|
||||
old.colorTheme = { name: 'illustrative', params: { style: { name: 'uniform', params: { value: old.colorTheme.params.value, lightness: old.colorTheme.params.lightness } } } };
|
||||
} else if (old.colorTheme.name === 'illustrative' && !values.illustrative) {
|
||||
old.colorTheme = { name: 'uniform', params: { value: old.colorTheme.params.style.params.value, lightness: old.colorTheme.params.style.params.lightness } };
|
||||
}
|
||||
}
|
||||
}).commit();
|
||||
};
|
||||
|
||||
updateLightness = (values: PD.Values) => {
|
||||
return this.plugin.build().to(this.ref).update(old => {
|
||||
if (old.colorTheme) {
|
||||
old.colorTheme.params.lightness = values.lightness;
|
||||
if (old.colorTheme.name === 'illustrative') {
|
||||
old.colorTheme.params.style.params.lightness = values.lightness;
|
||||
} else {
|
||||
old.colorTheme.params.lightness = values.lightness;
|
||||
}
|
||||
} else if (old.coloring) {
|
||||
old.coloring.params.lightness = values.lightness;
|
||||
}
|
||||
@@ -924,6 +1203,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
const depth = this.props.depth;
|
||||
const colorValue = this.colorValue;
|
||||
const lightnessValue = this.lightnessValue;
|
||||
const illustrativeValue = this.illustrativeValue;
|
||||
const opacityValue = this.opacityValue;
|
||||
const emissiveValue = this.emissiveValue;
|
||||
const lodValue = this.lodValue;
|
||||
@@ -953,6 +1233,7 @@ export class EntityNode extends Node<{}, { action?: 'color' | 'clip', isDisabled
|
||||
<ControlGroup header='Color' initialExpanded={true} hideExpander={true} hideOffset={true} onHeaderClick={this.toggleColor}
|
||||
topRightIcon={CloseSvg} noTopMargin childrenClassName='msp-viewport-controls-panel-controls'>
|
||||
<CombinedColorControl param={ColorValueParam} value={colorValue ?? Color(0xFFFFFF)} onChange={this.updateColor} name='color' hideNameRow />
|
||||
<ParameterControls params={IllustrativeParams} values={illustrativeValue} onChangeValues={this.updateIllustrative} />
|
||||
<ParameterControls params={LightnessParams} values={lightnessValue} onChangeValues={this.updateLightness} />
|
||||
<ParameterControls params={OpacityParams} values={opacityValue} onChangeValues={this.updateOpacity} />
|
||||
<ParameterControls params={EmissiveParams} values={emissiveValue} onChangeValues={this.updateEmissive} />
|
||||
|
||||
@@ -5,20 +5,63 @@
|
||||
*/
|
||||
|
||||
import { Mp4EncoderUI } from '../../../extensions/mp4-export/ui';
|
||||
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { CollapsableControls, CollapsableState, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { SectionHeader } from '../../../mol-plugin-ui/controls/common';
|
||||
import { ParameterControls } from '../../../mol-plugin-ui/controls/parameters';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
import { StructureMeasurementsControls } from '../../../mol-plugin-ui/structure/measurements';
|
||||
import { MesoscaleExplorerState } from '../app';
|
||||
import { MesoscaleState } from '../data/state';
|
||||
import { EntityControls, ModelInfo, SelectionInfo } from './entities';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls } from './states';
|
||||
import { EntityControls, FocusInfo, ModelInfo, SelectionInfo } from './entities';
|
||||
import { LoaderControls, ExampleControls, SessionControls, SnapshotControls, DatabaseControls, MesoQuickStylesControls, ExplorerInfo } from './states';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { TuneSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { RendererParams } from '../../../mol-gl/renderer';
|
||||
import { TrackballControlsParams } from '../../../mol-canvas3d/controls/trackball';
|
||||
|
||||
const Spacer = () => <div style={{ height: '2em' }} />;
|
||||
|
||||
const ViewportParams = {
|
||||
renderer: PD.Group(RendererParams),
|
||||
trackball: PD.Group(TrackballControlsParams),
|
||||
};
|
||||
|
||||
class ViewportSettingsUI extends CollapsableControls<{}, {}> {
|
||||
protected defaultState(): CollapsableState {
|
||||
return {
|
||||
header: 'Viewport Settings',
|
||||
isCollapsed: true,
|
||||
brand: { accent: 'cyan', svg: TuneSvg }
|
||||
};
|
||||
}
|
||||
|
||||
protected renderControls(): JSX.Element | null {
|
||||
return <>
|
||||
{this.plugin.canvas3d && this.plugin.canvas3dContext && <>
|
||||
<ParameterControls params={ViewportParams} values={this.plugin.canvas3d.props} onChange={this.setSettings} />
|
||||
</>}
|
||||
</>;
|
||||
}
|
||||
|
||||
private setSettings = (p: { param: PD.Base<any>, name: string, value: any }) => {
|
||||
PluginCommands.Canvas3D.SetSettings(this.plugin, { settings: { [p.name]: p.value } });
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
|
||||
this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
|
||||
}
|
||||
}
|
||||
|
||||
export class LeftPanel extends PluginUIComponent {
|
||||
render() {
|
||||
const customState = this.plugin.customState as MesoscaleExplorerState;
|
||||
|
||||
return <div className='msp-scrollable-container'>
|
||||
{customState.driver && <>
|
||||
<ExplorerInfo />
|
||||
<Spacer />
|
||||
</>}
|
||||
<SectionHeader title='Database' />
|
||||
<DatabaseControls />
|
||||
<Spacer />
|
||||
@@ -42,6 +85,7 @@ export class LeftPanel extends PluginUIComponent {
|
||||
<Spacer />
|
||||
|
||||
<Mp4EncoderUI />
|
||||
<ViewportSettingsUI />
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +103,13 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
);
|
||||
}
|
||||
|
||||
get hasFocusInfo() {
|
||||
return (
|
||||
MesoscaleState.has(this.plugin) &&
|
||||
!!(MesoscaleState.get(this.plugin).focusInfo !== '')
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
|
||||
this.setState({ isDisabled: v });
|
||||
@@ -89,10 +140,18 @@ export class RightPanel extends PluginUIComponent<{}, { isDisabled: boolean }> {
|
||||
<SectionHeader title='Selection' />
|
||||
<SelectionInfo />
|
||||
<Spacer />
|
||||
<StructureMeasurementsControls initiallyCollapsed={true}/>
|
||||
</>
|
||||
|
||||
<MesoQuickStylesControls />
|
||||
<Spacer />
|
||||
<SectionHeader title='Entities' />
|
||||
<EntityControls />
|
||||
<Spacer />
|
||||
{this.hasFocusInfo && <>
|
||||
<SectionHeader title='Focus Info' />
|
||||
<FocusInfo />
|
||||
<Spacer />
|
||||
</>}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
|
||||
import { MmcifProvider } from '../../../mol-plugin-state/formats/trajectory';
|
||||
import { StructureComponentManager } from '../../../mol-plugin-state/manager/structure/component';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { Button, ExpandGroup } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { Button, ExpandGroup, IconButton } from '../../../mol-plugin-ui/controls/common';
|
||||
import { GetAppSvg, HelpOutlineSvg, MagicWandSvg, TourSvg, Icon, OpenInBrowserSvg } from '../../../mol-plugin-ui/controls/icons';
|
||||
import { CollapsableControls, PluginUIComponent } from '../../../mol-plugin-ui/base';
|
||||
import { ApplyActionControl } from '../../../mol-plugin-ui/state/apply-action';
|
||||
import { LocalStateSnapshotList, LocalStateSnapshotParams, LocalStateSnapshots } from '../../../mol-plugin-ui/state/snapshots';
|
||||
import { PluginCommands } from '../../../mol-plugin/commands';
|
||||
@@ -24,7 +25,9 @@ import { createCellpackHierarchy } from '../data/cellpack/preset';
|
||||
import { createGenericHierarchy } from '../data/generic/preset';
|
||||
import { createMmcifHierarchy } from '../data/mmcif/preset';
|
||||
import { createPetworldHierarchy } from '../data/petworld/preset';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps } from '../data/state';
|
||||
import { MesoscaleState, MesoscaleStateObject, setGraphicsCanvas3DProps, updateColors } from '../data/state';
|
||||
import { isTimingMode } from '../../../mol-util/debug';
|
||||
import { now } from '../../../mol-util/now';
|
||||
|
||||
function adjustPluginProps(ctx: PluginContext) {
|
||||
ctx.managers.interactivity.setProps({ granularity: 'chain' });
|
||||
@@ -77,6 +80,7 @@ function adjustPluginProps(ctx: PluginContext) {
|
||||
radius: 5,
|
||||
bias: 1,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
@@ -162,14 +166,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) {
|
||||
@@ -269,7 +295,7 @@ export class DatabaseControls extends PluginUIComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{ margin: '5px' }}>
|
||||
return <div id='database' style={{ margin: '5px' }}>
|
||||
<ApplyActionControl state={this.plugin.state.data} action={LoadDatabase} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
|
||||
</div>;
|
||||
}
|
||||
@@ -281,7 +307,7 @@ export class LoaderControls extends PluginUIComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{ margin: '5px' }}>
|
||||
return <div id='loader' style={{ margin: '5px' }}>
|
||||
<ApplyActionControl state={this.plugin.state.data} action={LoadModel} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
|
||||
</div>;
|
||||
}
|
||||
@@ -293,7 +319,7 @@ export class ExampleControls extends PluginUIComponent {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div style={{ margin: '5px' }}>
|
||||
return <div id='example' style={{ margin: '5px' }}>
|
||||
<ApplyActionControl state={this.plugin.state.data} action={LoadExample} nodeRef={this.plugin.state.data.tree.root.ref} applyLabel={'Load'} hideHeader />
|
||||
</div>;
|
||||
}
|
||||
@@ -330,7 +356,7 @@ export class SessionControls extends PluginUIComponent {
|
||||
};
|
||||
|
||||
render() {
|
||||
return <div style={{ margin: '5px' }}>
|
||||
return <div id='session' style={{ margin: '5px' }}>
|
||||
<div className='msp-flex-row'>
|
||||
<Button icon={GetAppSvg} onClick={this.downloadToFileZip} title='Download the state.'>
|
||||
Download
|
||||
@@ -346,14 +372,14 @@ export class SessionControls extends PluginUIComponent {
|
||||
export class SnapshotControls extends PluginUIComponent<{}> {
|
||||
render() {
|
||||
return <div style={{ margin: '5px' }}>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div id='snaplist' style={{ marginBottom: '10px' }}>
|
||||
<LocalStateSnapshotList />
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div id='snap' style={{ marginBottom: '10px' }}>
|
||||
<LocalStateSnapshots />
|
||||
</div>
|
||||
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
<div id='snapoption' style={{ marginBottom: '10px' }}>
|
||||
<ExpandGroup header='Snapshot Options' initiallyExpanded={false}>
|
||||
<LocalStateSnapshotParams />
|
||||
</ExpandGroup>
|
||||
@@ -361,3 +387,496 @@ export class SnapshotControls extends PluginUIComponent<{}> {
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class ExplorerInfo extends PluginUIComponent<{}, { isDisabled: boolean, showHelp: boolean }> {
|
||||
state = {
|
||||
isDisabled: false,
|
||||
showHelp: false
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
this.subscribe(this.plugin.state.data.behaviors.isUpdating, v => {
|
||||
this.setState({ isDisabled: v });
|
||||
});
|
||||
|
||||
this.subscribe(this.plugin.state.events.cell.stateUpdated, e => {
|
||||
if (!this.state.isDisabled && MesoscaleState.has(this.plugin) && MesoscaleState.ref(this.plugin) === e.ref) {
|
||||
this.forceUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupDriver = () => {
|
||||
// setup the tour of the interface
|
||||
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
|
||||
if (!driver) return;
|
||||
|
||||
driver.setSteps([
|
||||
// Left panel
|
||||
{ element: '#explorerinfo', popover: { title: 'Explorer Header Info', description: 'This section displays the explorer header with version information, documentation access, and tour navigation. Use the right and left arrow keys to navigate the tour.', side: 'left', align: 'start' } },
|
||||
{ element: '#database', popover: { title: 'Import from PDB', description: 'Load structures directly from PDB and PDB-DEV databases.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#loader', popover: { title: 'Import from File', description: 'Load local files (.molx, .molj, .zip, .cif, .bcif) using this option.', side: 'bottom', align: 'start' } },
|
||||
{ element: '#example', popover: { title: 'Example Models and Tours', description: 'Select from a range of example models and tours provided.', side: 'left', align: 'start' } },
|
||||
{ element: '#session', popover: { title: 'Session Management', description: 'Download the current session in .molx format.', side: 'top', align: 'start' } },
|
||||
{ element: '#snaplist', popover: { title: 'Snapshot List', description: 'View and manage the list of snapshots. You can reorder them and edit their titles, keys, and descriptions. Snapshot states cannot be edited.', side: 'right', align: 'start' } },
|
||||
{ element: '#snap', popover: { title: 'Add Snapshot', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
|
||||
{ element: '#snapoption', popover: { title: 'Snapshot Options', description: 'These options are saved in the snapshot. Set them before adding a snapshot to see their effect during animation playback.', side: 'right', align: 'start' } },
|
||||
{ element: '#exportanimation', popover: { title: 'Export Animation', description: 'Create movies or scenes with rocking, rotating, or snapshots animations.', side: 'right', align: 'start' } },
|
||||
{ element: '#viewportsettings', popover: { title: 'Viewport Settings', description: 'Advanced settings for the renderer and trackball.', side: 'right', align: 'start' } },
|
||||
// Viewport
|
||||
{ element: '#snapinfo', popover: { title: 'Snapshot Description', description: 'Save the current state (e.g., camera position, color, visibility, etc.) in a snapshot with an optional title, key, and description.', side: 'right', align: 'start' } },
|
||||
{ element: '#snapinfoctrl', popover: { title: 'Snapshot Description Control', description: 'Control the visibility and text size of the snapshot description widget.', side: 'right', align: 'start' } },
|
||||
// Right panel
|
||||
{ element: '#modelinfo', popover: { title: 'Model Information', description: 'Summary information about the model, if available.', side: 'right', align: 'start' } },
|
||||
{ element: '#selestyle', popover: { title: 'Selection Style', description: 'Choose the rendering style for entity selection accessed via Shift/Ctrl mouse. Options include: Color & Outline, Color, Outline.', side: 'right', align: 'start' } },
|
||||
{ element: '#seleinfo', popover: { title: 'Selection List', description: 'View the current list of selected entities.', side: 'right', align: 'start' } },
|
||||
{ element: '#measurements', popover: { title: 'Measurements', description: 'Use this widget to create labels, measure distances, angles, dihedral orientations, and planes for the selected entities.', side: 'right', align: 'start' } },
|
||||
{ element: '#quickstyles', popover: { title: 'Quick Styles', description: 'Change between a selection of style presets.', side: 'right', align: 'start' } },
|
||||
{ element: '#graphicsquality', popover: { title: 'Graphics Quality', description: 'Adjust the overall graphics quality. Lower quality improves performance. Options are: Ultra, Quality (Default), Balanced, Performance, Custom. Custom settings use the Culling & LOD values set in the Tree.', side: 'right', align: 'start' } },
|
||||
{ element: '#searchtree', popover: { title: 'Search', description: 'Filter the entity tree based on your queries.', side: 'right', align: 'start' } },
|
||||
{ element: '#grouptree', popover: { title: 'Group By', description: 'Change the grouping of the hierarchy tree, e.g., group by instance or by compartment.', side: 'right', align: 'start' } },
|
||||
{ element: '#tree', popover: { title: 'Tree Hierarchy', description: 'View the hierarchical tree of entity types in the model.', side: 'right', align: 'start' } },
|
||||
{ element: '#focusinfo', popover: { title: 'Selection Description', description: 'Detailed information about the current selection, if present in the loaded file.', side: 'right', align: 'start' } },
|
||||
{ popover: { title: 'Happy Exploring!', description: 'That’s all! Go ahead and start exploring or creating mesoscale tours.' } }
|
||||
]);
|
||||
driver.refresh();
|
||||
};
|
||||
|
||||
openHelp = () => {
|
||||
// open a new page with the documentation
|
||||
window.open('https://molstar.org/me-docs/', '_blank');
|
||||
};
|
||||
|
||||
toggleHelp = () => {
|
||||
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
|
||||
if (!driver || !driver.hasNextStep()) {
|
||||
this.setupDriver();
|
||||
}
|
||||
this.setState({ showHelp: !this.state.showHelp }, () => {
|
||||
if (this.state.showHelp && driver) {
|
||||
driver.drive(); // start at 0
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const driver = (this.plugin.customState as MesoscaleExplorerState).driver;
|
||||
if (!driver) return;
|
||||
|
||||
const help = <IconButton svg={HelpOutlineSvg} toggleState={false} small onClick={this.openHelp} title='Open the Documentation' />;
|
||||
const tour = <IconButton svg={TourSvg} toggleState={false} small onClick={this.toggleHelp} title='Start the interactive tour' />;
|
||||
return <>
|
||||
<div id='explorerinfo' style={{ display: 'flex', alignItems: 'center', padding: '4px 0 4px 8px' }} className='msp-help-text'>
|
||||
<h2 style={{ flexGrow: 1 }}>Mol* Mesoscale Explorer</h2>
|
||||
{tour}{help}
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MesoQuickStylesControls extends CollapsableControls {
|
||||
defaultState() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
header: 'Quick Styles',
|
||||
brand: { accent: 'gray' as const, svg: MagicWandSvg }
|
||||
};
|
||||
}
|
||||
|
||||
renderControls() {
|
||||
return <>
|
||||
<MesoQuickStyles />
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
export class MesoQuickStyles extends PluginUIComponent {
|
||||
state = {
|
||||
celShaded: false,
|
||||
};
|
||||
default_color_values = {
|
||||
type: 'group-generate',
|
||||
illustrative: false,
|
||||
value: [1, 1, 1, 1],
|
||||
variability: 20,
|
||||
shift: 0,
|
||||
lightness: 0,
|
||||
alpha: 1,
|
||||
emissive: 0
|
||||
};
|
||||
illustrative_color_values = {
|
||||
type: 'group-generate',
|
||||
illustrative: true,
|
||||
value: [1, 1, 1, 1],
|
||||
variability: 20,
|
||||
shift: 0,
|
||||
lightness: 0,
|
||||
alpha: 1,
|
||||
emissive: 0
|
||||
};
|
||||
async default() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.6,
|
||||
maxDistance: 80,
|
||||
steps: 3,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.15,
|
||||
color: Color(0x000000),
|
||||
includeTransparent: false,
|
||||
}
|
||||
},
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
|
||||
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
}
|
||||
|
||||
async celshading() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.5,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: { name: 'off', params: {} },
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: true, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
}
|
||||
|
||||
async stylizedDof() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: { name: 'off', params: {} },
|
||||
dof: {
|
||||
name: 'on',
|
||||
params: {
|
||||
blurSize: 9,
|
||||
blurSpread: 1.0,
|
||||
inFocus: 0.0,
|
||||
PPM: 200.0,
|
||||
center: 'camera-target',
|
||||
mode: 'sphere',
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
}
|
||||
|
||||
async illustrative() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.5,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.15,
|
||||
color: Color(0x000000),
|
||||
includeTransparent: false,
|
||||
}
|
||||
},
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: true, materialStyle: { metalness: 0, roughness: 1.0, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false, };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.illustrative_color_values, options);
|
||||
}
|
||||
|
||||
async shiny() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.5,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: { name: 'off', params: {} },
|
||||
outline: { name: 'off', params: {} },
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.default_color_values, options);
|
||||
}
|
||||
|
||||
async stylized() {
|
||||
if (!this.plugin.canvas3d) return;
|
||||
this.plugin.canvas3d.setProps({
|
||||
renderer: {
|
||||
exposure: 1.1,
|
||||
},
|
||||
postprocessing: {
|
||||
occlusion: {
|
||||
name: 'on',
|
||||
params: {
|
||||
samples: 32,
|
||||
multiScale: {
|
||||
name: 'on',
|
||||
params: {
|
||||
levels: [
|
||||
{ radius: 2, bias: 1.0 },
|
||||
{ radius: 5, bias: 1.0 },
|
||||
{ radius: 8, bias: 1.0 },
|
||||
{ radius: 11, bias: 1.0 },
|
||||
],
|
||||
nearThreshold: 10,
|
||||
farThreshold: 1500,
|
||||
}
|
||||
},
|
||||
radius: 5,
|
||||
bias: 1.3,
|
||||
blurKernelSize: 11,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
},
|
||||
shadow: {
|
||||
name: 'on',
|
||||
params: {
|
||||
bias: 0.4,
|
||||
maxDistance: 256,
|
||||
steps: 64,
|
||||
tolerance: 1.0,
|
||||
}
|
||||
},
|
||||
outline: {
|
||||
name: 'on',
|
||||
params: {
|
||||
scale: 1,
|
||||
threshold: 0.15,
|
||||
color: Color(0x000000),
|
||||
includeTransparent: false,
|
||||
}
|
||||
},
|
||||
dof: { name: 'off', params: {} },
|
||||
}
|
||||
});
|
||||
// ignore Light
|
||||
const loptions = { ignoreLight: false, materialStyle: { metalness: 0, roughness: 0.2, bumpiness: 0 } };
|
||||
const options = { ...loptions, celShaded: false };
|
||||
await this.plugin.managers.structure.component.setOptions(loptions as StructureComponentManager.Options);
|
||||
await updateColors(this.plugin, this.illustrative_color_values, options);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow title='Applies default representation preset and sets outline and occlusion effects to default' onClick={() => this.default()} style={{ width: 'auto' }}>
|
||||
Default
|
||||
</Button>
|
||||
<Button noOverflow title='Applies celShading' onClick={() => this.celshading()} style={{ width: 'auto' }}>
|
||||
Cel-shaded
|
||||
</Button>
|
||||
<Button noOverflow title='Applies illustrative colors preset' onClick={() => this.illustrative()} style={{ width: 'auto' }}>
|
||||
Illustrative
|
||||
</Button>
|
||||
</div>
|
||||
<div className='msp-flex-row'>
|
||||
<Button noOverflow title='Apply shiny material to default' onClick={() => this.shiny()} style={{ width: 'auto' }}>
|
||||
Shiny
|
||||
</Button>
|
||||
<Button noOverflow title='Enable shiny material, outline, and illustrative colors' onClick={() => this.stylized()} style={{ width: 'auto' }}>
|
||||
Shiny-Illustrative
|
||||
</Button>
|
||||
<Button noOverflow title='Enable DOF and shiny material' onClick={() => this.stylizedDof()} style={{ width: 'auto' }}>
|
||||
Shiny-DOF
|
||||
</Button>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Neli Fonseca <neli@ebi.ac.uk>
|
||||
*/
|
||||
|
||||
import { ANVILMembraneOrientation } from '../../extensions/anvil/behavior';
|
||||
@@ -21,7 +22,7 @@ import { MVSData } from '../../extensions/mvs/mvs-data';
|
||||
import { PDBeStructureQualityReport } from '../../extensions/pdbe';
|
||||
import { RCSBValidationReport } from '../../extensions/rcsb';
|
||||
import { AssemblySymmetry, AssemblySymmetryConfig } from '../../extensions/assembly-symmetry';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider } from '../../extensions/sb-ncbr';
|
||||
import { SbNcbrPartialCharges, SbNcbrPartialChargesPreset, SbNcbrPartialChargesPropertyProvider, SbNcbrTunnels } from '../../extensions/sb-ncbr';
|
||||
import { Volseg, VolsegVolumeServerConfig } from '../../extensions/volumes-and-segmentations';
|
||||
import { wwPDBChemicalComponentDictionary } from '../../extensions/wwpdb/ccd/behavior';
|
||||
import { wwPDBStructConnExtensionFunctions } from '../../extensions/wwpdb/struct-conn';
|
||||
@@ -81,6 +82,7 @@ export const ExtensionMap = {
|
||||
'sb-ncbr-partial-charges': PluginSpec.Behavior(SbNcbrPartialCharges),
|
||||
'wwpdb-chemical-component-dictionary': PluginSpec.Behavior(wwPDBChemicalComponentDictionary),
|
||||
'mvs': PluginSpec.Behavior(MolViewSpec),
|
||||
'tunnels': PluginSpec.Behavior(SbNcbrTunnels),
|
||||
};
|
||||
|
||||
const DefaultViewerOptions = {
|
||||
@@ -316,7 +318,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'
|
||||
@@ -422,6 +427,34 @@ export class Viewer {
|
||||
});
|
||||
}
|
||||
|
||||
loadFullResolutionEMDBMap(emdbId: string, options: { isoValue: Volume.IsoValue, color?: Color }) {
|
||||
const plugin = this.plugin;
|
||||
const numericId = parseInt(emdbId.toUpperCase().replace('EMD-', ''));
|
||||
const url = `https://ftp.ebi.ac.uk/pub/databases/emdb/structures/EMD-${numericId}/map/emd_${numericId}.map.gz`;
|
||||
|
||||
return plugin.dataTransaction(async () => {
|
||||
const data = await plugin.build().toRoot()
|
||||
.apply(StateTransforms.Data.Download, { url, isBinary: true, label: emdbId }, { state: { isGhost: true } })
|
||||
.apply(StateTransforms.Data.DeflateData)
|
||||
.commit();
|
||||
|
||||
const parsed = await plugin.dataFormats.get('ccp4')!.parse(plugin, data, { entryId: emdbId });
|
||||
const firstVolume = (parsed.volume || parsed.volumes[0]) as StateObjectSelector<PluginStateObject.Volume.Data>;
|
||||
if (!firstVolume?.isOk) throw new Error('Failed to parse any volume.');
|
||||
|
||||
const volume: StateObjectSelector<PluginStateObject.Volume.Data> = parsed.volumes?.[0] ?? parsed.volume;
|
||||
await plugin.build()
|
||||
.to(volume)
|
||||
.apply(StateTransforms.Representation.VolumeRepresentation3D, createVolumeRepresentationParams(this.plugin, firstVolume.data!, {
|
||||
type: 'isosurface',
|
||||
typeParams: { alpha: 1, isoValue: options.isoValue },
|
||||
color: 'uniform',
|
||||
colorParams: { value: options.color ?? Color(0x33BB33) }
|
||||
}))
|
||||
.commit();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* viewer.loadTrajectory({
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -33,6 +33,7 @@ const Canvas3DPresets = {
|
||||
radius: 5,
|
||||
bias: 0.8,
|
||||
blurKernelSize: 15,
|
||||
blurDepthBias: 0.5,
|
||||
resolutionScale: 1,
|
||||
color: Color(0x000000),
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2021-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Mandar Deshpande <mandar@ebi.ac.uk>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
@@ -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';
|
||||
@@ -41,8 +41,13 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
const getColor = (location: StructureElement.Location): Color => {
|
||||
const { unit, element } = location;
|
||||
if (!Unit.isAtomic(unit)) return DefaultColor;
|
||||
|
||||
const qualityAssessment = QualityAssessmentProvider.get(unit.model).value;
|
||||
const score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]) ?? -1;
|
||||
let score = qualityAssessment?.pLDDT?.get(unit.model.atomicHierarchy.residueAtomSegments.index[element]);
|
||||
if (typeof score !== 'number') {
|
||||
score = unit.model.atomicConformation.B_iso_or_equiv.value(element);
|
||||
}
|
||||
|
||||
if (score < 0) {
|
||||
return DefaultColor;
|
||||
} else if (score <= 50) {
|
||||
@@ -74,7 +79,7 @@ export function PLDDTConfidenceColorTheme(ctx: ThemeDataContext, props: PD.Value
|
||||
preferSmoothing: true,
|
||||
color,
|
||||
props,
|
||||
description: 'Assigns residue colors according to the pLDDT Confidence score.',
|
||||
description: 'Assigns residue colors according to the pLDDT Confidence score. If no Model Archive quality assessment score is available, the B-factor value is used instead.',
|
||||
legend: ConfidenceColorLegend
|
||||
};
|
||||
}
|
||||
@@ -86,7 +91,7 @@ export const PLDDTConfidenceColorThemeProvider: ColorTheme.Provider<PLDDTConfide
|
||||
factory: PLDDTConfidenceColorTheme,
|
||||
getParams: getPLDDTConfidenceColorThemeParams,
|
||||
defaultValues: PD.getDefaultValues(getPLDDTConfidenceColorThemeParams({})),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT')),
|
||||
isApplicable: (ctx: ThemeDataContext) => !!ctx.structure?.models.some(m => QualityAssessment.isApplicable(m, 'pLDDT') || (m.atomicConformation.B_iso_or_equiv.isDefined && !Model.isExperimental(m))),
|
||||
ensureCustomProperties: {
|
||||
attach: async (ctx: CustomProperty.Context, data: ThemeDataContext) => {
|
||||
if (data.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 David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -72,11 +72,11 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
|
||||
await params.pass.updateBackground();
|
||||
|
||||
await plugin.managers.animation.play(params.animation.definition, params.animation.params);
|
||||
|
||||
stoppedAnimation = false;
|
||||
for (let i = 0; i <= N; i++) {
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true });
|
||||
|
||||
const image = params.pass.getImageData(width, height, normalizedViewport);
|
||||
await loop.tick(i * dt, { isSynchronous: true, animation: { currentFrame: i, frameCount: N }, manualDraw: true, updateControls: true });
|
||||
const image = await params.pass.getImageData(ctx, width, height, normalizedViewport);
|
||||
encoder.addFrameRgba(image.data);
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dominik Tichý <tichydominik451@gmail.com>
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
export { SbNcbrPartialCharges } from './partial-charges/behavior';
|
||||
export { SbNcbrPartialChargesPreset } from './partial-charges/preset';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
export { SbNcbrPartialChargesPropertyProvider } from './partial-charges/property';
|
||||
export { SbNcbrTunnels } from './tunnels/behavior';
|
||||
export { TunnelsFromRawData, SelectTunnel, TunnelFromRawData, TunnelShapeProvider } from './tunnels/representation';
|
||||
99
src/extensions/sb-ncbr/tunnels/actions.ts
Normal file
99
src/extensions/sb-ncbr/tunnels/actions.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateAction } from '../../../mol-state';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { Asset } from '../../../mol-util/assets';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
|
||||
import { TunnelsServerConfig } from './props';
|
||||
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider } from './representation';
|
||||
|
||||
export const TunnelDownloadServer = {
|
||||
'channelsdb': PD.EmptyGroup({ label: 'ChannelsDB' })
|
||||
};
|
||||
|
||||
type DownloadTunnels = typeof DownloadTunnels
|
||||
export const DownloadTunnels = StateAction.build({
|
||||
display: { name: 'Download Tunnels', description: 'Load a tunnels from the provided source and create theirs representations' },
|
||||
from: PluginStateObject.Root,
|
||||
params: (_, plugin: PluginContext) => {
|
||||
return {
|
||||
source: PD.MappedStatic('pdb', {
|
||||
'pdb': PD.Group({
|
||||
provider: PD.Group({
|
||||
id: PD.Text('1tqn', { label: 'PDB Id(s)', description: 'One or more comma/space separated PDB ids.' }),
|
||||
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
|
||||
}, { pivot: 'id' }),
|
||||
}, { isFlat: true, label: 'PDB' }),
|
||||
'alphafolddb': PD.Group({
|
||||
provider: PD.Group({
|
||||
id: PD.Text('Q8W3K0', { label: 'UniProtKB AC(s)', description: 'One or more comma/space separated ACs.' }),
|
||||
server: PD.MappedStatic('channelsdb', TunnelDownloadServer),
|
||||
}, { pivot: 'id' })
|
||||
}, { isFlat: true, label: 'AlphaFold DB', description: 'Loads the predicted model if available' }),
|
||||
'url': PD.Group({
|
||||
url: PD.Url(''),
|
||||
}, { isFlat: true, label: 'URL' })
|
||||
})
|
||||
};
|
||||
}
|
||||
})(({ params, state }, plugin: PluginContext) => Task.create('Download Tunnels', async ctx => {
|
||||
plugin.behaviors.layout.leftPanelTabName.next('data');
|
||||
|
||||
const src = params.source;
|
||||
let downloadParams: PD.Normalize<{ url: string | Asset.Url }>[];
|
||||
|
||||
switch (src.name) {
|
||||
case 'url':
|
||||
downloadParams = [{ url: src.params.url }];
|
||||
break;
|
||||
case 'pdb':
|
||||
downloadParams = src.params.provider.server.name === 'channelsdb'
|
||||
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/pdb/${src.params.provider.id}` }]
|
||||
: assertUnreachable(src as never);
|
||||
break;
|
||||
case 'alphafolddb':
|
||||
downloadParams = src.params.provider.server.name === 'channelsdb'
|
||||
? [{ url: `${plugin?.config.get(TunnelsServerConfig.DefaultServerUrl)}/channels/alphafill/${src.params.provider.id.toLowerCase()}` }]
|
||||
: assertUnreachable(src as never);
|
||||
break;
|
||||
default: assertUnreachable(src);
|
||||
}
|
||||
|
||||
await state.transaction(async () => {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
for (const download of downloadParams) {
|
||||
const response = await (await fetch(download.url.toString())).json();
|
||||
const tunnels: Tunnel[] = [];
|
||||
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
}).runInContext(ctx);
|
||||
}));
|
||||
607
src/extensions/sb-ncbr/tunnels/algorithm.ts
Normal file
607
src/extensions/sb-ncbr/tunnels/algorithm.ts
Normal file
@@ -0,0 +1,607 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { OrderedSet } from '../../../mol-data/int';
|
||||
import { addSphere } from '../../../mol-geo/geometry/mesh/builder/sphere';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { MeshBuilder } from '../../../mol-geo/geometry/mesh/mesh-builder';
|
||||
import { computeMarchingCubesMesh } from '../../../mol-geo/util/marching-cubes/algorithm';
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../../mol-gl/webgl/texture';
|
||||
import { PositionData, Sphere3D, Box3D, GridLookup3D, fillGridDim } from '../../../mol-math/geometry';
|
||||
import { Boundary, getBoundary } from '../../../mol-math/geometry/boundary';
|
||||
import { DefaultMolecularSurfaceCalculationProps, MolecularSurfaceCalculationProps } from '../../../mol-math/geometry/molecular-surface';
|
||||
import { lerp, spline } from '../../../mol-math/interpolate';
|
||||
import { Vec3, Tensor, Mat4 } from '../../../mol-math/linear-algebra';
|
||||
import { Shape } from '../../../mol-model/shape';
|
||||
import { ensureReasonableResolution } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { Task, RuntimeContext } from '../../../mol-task';
|
||||
import { ValueCell } from '../../../mol-util';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { Tunnel, Profile } from './data-model';
|
||||
|
||||
type MolecularSurfaceMeta = {
|
||||
resolution?: number
|
||||
colorTexture?: Texture
|
||||
}
|
||||
|
||||
export async function createSpheresShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, showRadii: boolean, prev?: Shape<Mesh> }) {
|
||||
const builder = MeshBuilder.createState(512, 512, options.prev?.geometry);
|
||||
const tunnel = options.tunnel;
|
||||
|
||||
const processedData = interpolateTunnel(tunnel.data, options.sampleRate);
|
||||
|
||||
if (options.showRadii) {
|
||||
for (let i = 0; i < processedData.length; i += 1) {
|
||||
const p = processedData[i];
|
||||
builder.currentGroup = i;
|
||||
const center = [p.X, p.Y, p.Z];
|
||||
addSphere(builder, center as Vec3, p.Radius, options.resolution);
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < processedData.length; i += 1) {
|
||||
const p = processedData[i];
|
||||
builder.currentGroup = 0;
|
||||
const center = [p.X, p.Y, p.Z];
|
||||
addSphere(builder, center as Vec3, p.Radius, options.resolution);
|
||||
}
|
||||
}
|
||||
|
||||
const mesh = MeshBuilder.getMesh(builder);
|
||||
const name = tunnel.props.highlight_label ?
|
||||
tunnel.props.highlight_label :
|
||||
tunnel.props.type && tunnel.props.id ?
|
||||
`${tunnel.props.type} ${tunnel.props.id}` :
|
||||
'Tunnel';
|
||||
|
||||
if (options.showRadii)
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
(i) => `[${processedData[i].X.toFixed(3)}, ${processedData[i].Y.toFixed(3)}, ${processedData[i].Z.toFixed(3)}] - radius: ${processedData[i].Radius.toFixed(3)}`,
|
||||
);
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
() => name,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createTunnelShape(options: { tunnel: Tunnel, color: Color, resolution: number, sampleRate: number, webgl: WebGLContext | undefined, prev?: Shape<Mesh> }) {
|
||||
const tunnel = options.tunnel;
|
||||
const mesh = await createTunnelMesh(tunnel.data, {
|
||||
detail: options.resolution,
|
||||
sampleRate: options.sampleRate,
|
||||
webgl: options.webgl,
|
||||
prev: options.prev?.geometry
|
||||
});
|
||||
|
||||
const name = tunnel.props.highlight_label ?
|
||||
tunnel.props.highlight_label :
|
||||
tunnel.props.type && tunnel.props.id ?
|
||||
`${tunnel.props.type} ${tunnel.props.id}` :
|
||||
'Tunnel';
|
||||
|
||||
return Shape.create(
|
||||
name,
|
||||
tunnel.props,
|
||||
mesh,
|
||||
() => Color(options.color),
|
||||
() => 1,
|
||||
() => name,
|
||||
);
|
||||
}
|
||||
|
||||
function profileToVec3(profile: Profile): Vec3 {
|
||||
return Vec3.create(profile.X, profile.Y, profile.Z);
|
||||
}
|
||||
|
||||
// Centripetal Catmull–Rom spline interpolation
|
||||
function interpolateTunnel(profile: Profile[], sampleRate: number) {
|
||||
const interpolatedProfiles: Profile[] = [];
|
||||
if (profile.length < 4) return profile; // Ensuring there are enough points to interpolate
|
||||
|
||||
interpolatedProfiles.push(profile[0]);
|
||||
|
||||
let lastPoint = profileToVec3(profile[0]);
|
||||
let currentDistance = 0;
|
||||
const pointInterval = 1 / sampleRate;
|
||||
|
||||
for (let i = 1; i < profile.length - 2; i++) {
|
||||
const P0 = profile[i - 1];
|
||||
const P1 = profile[i];
|
||||
const P2 = profile[i + 1];
|
||||
const P3 = profile[i + 2];
|
||||
|
||||
for (let t = 0; t <= 1; t += 0.05) {
|
||||
const interpolatedX = spline(P0.X, P1.X, P2.X, P3.X, t, 0.5);
|
||||
const interpolatedY = spline(P0.Y, P1.Y, P2.Y, P3.Y, t, 0.5);
|
||||
const interpolatedZ = spline(P0.Z, P1.Z, P2.Z, P3.Z, t, 0.5);
|
||||
const interpolatedPoint = Vec3.create(interpolatedX, interpolatedY, interpolatedZ);
|
||||
|
||||
const distanceToAdd = Vec3.distance(lastPoint, interpolatedPoint);
|
||||
currentDistance += distanceToAdd;
|
||||
|
||||
if (currentDistance >= pointInterval) {
|
||||
interpolatedProfiles.push({
|
||||
X: interpolatedX,
|
||||
Y: interpolatedY,
|
||||
Z: interpolatedZ,
|
||||
Radius: spline(P0.Radius, P1.Radius, P2.Radius, P3.Radius, t, 0.5),
|
||||
Charge: lerp(P1.Charge, P2.Charge, t),
|
||||
FreeRadius: spline(P0.FreeRadius, P1.FreeRadius, P2.FreeRadius, P3.FreeRadius, t, 0.5),
|
||||
T: lerp(P1.T, P2.T, t),
|
||||
Distance: lerp(P1.Distance, P2.Distance, t)
|
||||
});
|
||||
lastPoint = interpolatedPoint;
|
||||
currentDistance -= pointInterval;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensuring the last profile point is included
|
||||
interpolatedProfiles.push(profile[profile.length - 1]);
|
||||
|
||||
return interpolatedProfiles;
|
||||
}
|
||||
|
||||
|
||||
function convertToPositionData(profile: Profile[], probeRadius: number): Required<PositionData> {
|
||||
let position = {} as PositionData;
|
||||
|
||||
const x: number[] = [];
|
||||
const y: number[] = [];
|
||||
const z: number[] = [];
|
||||
const indices: Array<number> = [];
|
||||
const radius: number[] = [];
|
||||
|
||||
let maxRadius: number = Number.MIN_SAFE_INTEGER;
|
||||
|
||||
let sphereCounter = 0;
|
||||
for (const sphere of profile) {
|
||||
x.push(sphere.X);
|
||||
y.push(sphere.Y);
|
||||
z.push(sphere.Z);
|
||||
indices.push(sphereCounter);
|
||||
radius.push(sphere.Radius + probeRadius);
|
||||
if (sphere.Radius > maxRadius) maxRadius = sphere.Radius;
|
||||
sphereCounter++;
|
||||
}
|
||||
|
||||
position = { x, y, z, indices: OrderedSet.ofSortedArray(indices), radius, id: indices };
|
||||
|
||||
return position as Required<PositionData>;
|
||||
}
|
||||
|
||||
async function createTunnelMesh(
|
||||
data: Profile[],
|
||||
options: {
|
||||
detail: number,
|
||||
sampleRate: number,
|
||||
webgl?: WebGLContext,
|
||||
prev?: Mesh
|
||||
}
|
||||
) {
|
||||
const props = {
|
||||
...DefaultMolecularSurfaceCalculationProps,
|
||||
};
|
||||
const preprocessedData = interpolateTunnel(data, options.sampleRate);
|
||||
const positions = convertToPositionData(preprocessedData, props.probeRadius);
|
||||
const bounds: Boundary = getBoundary(positions);
|
||||
|
||||
let maxR = 0;
|
||||
for (let i = 0; i < positions.radius.length; ++i) {
|
||||
const r = positions.radius[i];
|
||||
if (maxR < r) maxR = r;
|
||||
}
|
||||
|
||||
const p = ensureReasonableResolution(bounds.box, props);
|
||||
|
||||
const { field, transform, /* resolution,*/ maxRadius, /* idField */ } = await computeTunnelSurface(
|
||||
{
|
||||
positions,
|
||||
boundary: bounds,
|
||||
maxRadius: maxR,
|
||||
box: bounds.box,
|
||||
props: p
|
||||
}
|
||||
).run();
|
||||
|
||||
const params = {
|
||||
isoLevel: p.probeRadius,
|
||||
scalarField: field,
|
||||
};
|
||||
const surface = await computeMarchingCubesMesh(params, options.prev).run();
|
||||
const iterations = Math.ceil(2 / 1);
|
||||
Mesh.smoothEdges(surface, { iterations, maxNewEdgeLength: Math.sqrt(2) });
|
||||
|
||||
Mesh.transform(surface, transform);
|
||||
if (options.webgl && !options.webgl.isWebGL2) {
|
||||
Mesh.uniformTriangleGroup(surface);
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, false);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(surface.varyingGroup, true);
|
||||
}
|
||||
|
||||
const sphere = Sphere3D.expand(Sphere3D(), bounds.sphere, maxRadius);
|
||||
surface.setBoundingSphere(sphere);
|
||||
(surface.meta as MolecularSurfaceMeta).resolution = options.detail;
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
function normalToLine(out: Vec3, p: Vec3) {
|
||||
out[0] = out[1] = out[2] = 1.0;
|
||||
if (p[0] !== 0) {
|
||||
out[0] = (p[1] + p[2]) / -p[0];
|
||||
} else if (p[1] !== 0) {
|
||||
out[1] = (p[0] + p[2]) / -p[1];
|
||||
} else if (p[2] !== 0) {
|
||||
out[2] = (p[0] + p[1]) / -p[2];
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function computeTunnelSurface(
|
||||
surfaceData: {
|
||||
positions: Required<PositionData>,
|
||||
boundary: Boundary,
|
||||
maxRadius: number,
|
||||
box: Box3D | null,
|
||||
props: MolecularSurfaceCalculationProps
|
||||
}
|
||||
) {
|
||||
return Task.create('Tunnel Surface', async (ctx) => {
|
||||
return await calcTunnelSurface(ctx, surfaceData);
|
||||
});
|
||||
}
|
||||
|
||||
type AnglesTables = { cosTable: Float32Array, sinTable: Float32Array }
|
||||
function getAngleTables(probePositions: number): AnglesTables {
|
||||
let theta = 0.0;
|
||||
const step = 2 * Math.PI / probePositions;
|
||||
|
||||
const cosTable = new Float32Array(probePositions);
|
||||
const sinTable = new Float32Array(probePositions);
|
||||
for (let i = 0; i < probePositions; i++) {
|
||||
cosTable[i] = Math.cos(theta);
|
||||
sinTable[i] = Math.sin(theta);
|
||||
theta += step;
|
||||
}
|
||||
return { cosTable, sinTable };
|
||||
}
|
||||
|
||||
// From '../../../\mol-math\geometry\molecular-surface.ts'
|
||||
async function calcTunnelSurface(ctx: RuntimeContext,
|
||||
surfaceData: {
|
||||
positions: Required<PositionData>,
|
||||
boundary: Boundary,
|
||||
maxRadius: number,
|
||||
box: Box3D | null,
|
||||
props: MolecularSurfaceCalculationProps
|
||||
}) {
|
||||
// Field generation method adapted from AstexViewer (Mike Hartshorn) by Fred Ludlow.
|
||||
// Other parts based heavily on NGL (Alexander Rose) EDT Surface class
|
||||
|
||||
let lastClip = -1;
|
||||
|
||||
/**
|
||||
* Is the point at x,y,z obscured by any of the atoms specifeid by indices in neighbours.
|
||||
* Ignore indices a and b (these are the relevant atoms in projectPoints/Torii)
|
||||
*
|
||||
* Cache the last clipped atom (as very often the same one in subsequent calls)
|
||||
*
|
||||
* `a` and `b` must be resolved indices
|
||||
*/
|
||||
function obscured(x: number, y: number, z: number, a: number, b: number) {
|
||||
if (lastClip !== -1) {
|
||||
const ai = lastClip;
|
||||
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
|
||||
return ai;
|
||||
} else {
|
||||
lastClip = -1;
|
||||
}
|
||||
}
|
||||
|
||||
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
|
||||
const ai = OrderedSet.getAt(indices, neighbours.indices[j]);
|
||||
if (ai !== a && ai !== b && singleAtomObscures(ai, x, y, z)) {
|
||||
lastClip = ai;
|
||||
return ai;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* `ai` must be a resolved index
|
||||
*/
|
||||
function singleAtomObscures(ai: number, x: number, y: number, z: number) {
|
||||
const r = radius[ai];
|
||||
const dx = px[ai] - x;
|
||||
const dy = py[ai] - y;
|
||||
const dz = pz[ai] - z;
|
||||
const dSq = dx * dx + dy * dy + dz * dz;
|
||||
return dSq < (r * r);
|
||||
}
|
||||
|
||||
/**
|
||||
* For each atom:
|
||||
* Iterate over a subsection of the grid, for each point:
|
||||
* If current value < 0.0, unvisited, set positive
|
||||
*
|
||||
* In any case: Project this point onto surface of the atomic sphere
|
||||
* If this projected point is not obscured by any other atom
|
||||
* Calculate delta distance and set grid value to minimum of
|
||||
* itself and delta
|
||||
*/
|
||||
function projectPointsRange(begI: number, endI: number) {
|
||||
for (let i = begI; i < endI; ++i) {
|
||||
const j = OrderedSet.getAt(indices, i);
|
||||
const vx = px[j], vy = py[j], vz = pz[j];
|
||||
const rad = radius[j];
|
||||
const rSq = rad * rad;
|
||||
|
||||
lookup3d.find(vx, vy, vz, rad);
|
||||
|
||||
// Number of grid points, round this up...
|
||||
const ng = Math.ceil(rad * scaleFactor);
|
||||
|
||||
// Center of the atom, mapped to grid points (take floor)
|
||||
const iax = Math.floor(scaleFactor * (vx - minX));
|
||||
const iay = Math.floor(scaleFactor * (vy - minY));
|
||||
const iaz = Math.floor(scaleFactor * (vz - minZ));
|
||||
|
||||
// Extents of grid to consider for this atom
|
||||
const begX = Math.max(0, iax - ng);
|
||||
const begY = Math.max(0, iay - ng);
|
||||
const begZ = Math.max(0, iaz - ng);
|
||||
|
||||
// Add two to these points:
|
||||
// - iax are floor'd values so this ensures coverage
|
||||
// - these are loop limits (exclusive)
|
||||
const endX = Math.min(dimX, iax + ng + 2);
|
||||
const endY = Math.min(dimY, iay + ng + 2);
|
||||
const endZ = Math.min(dimZ, iaz + ng + 2);
|
||||
|
||||
for (let xi = begX; xi < endX; ++xi) {
|
||||
const dx = gridx[xi] - vx;
|
||||
const xIdx = xi * iuv;
|
||||
for (let yi = begY; yi < endY; ++yi) {
|
||||
const dy = gridy[yi] - vy;
|
||||
const dxySq = dx * dx + dy * dy;
|
||||
const xyIdx = yi * iu + xIdx;
|
||||
for (let zi = begZ; zi < endZ; ++zi) {
|
||||
const dz = gridz[zi] - vz;
|
||||
const dSq = dxySq + dz * dz;
|
||||
|
||||
if (dSq < rSq) {
|
||||
const idx = zi + xyIdx;
|
||||
|
||||
// if unvisited, make positive
|
||||
if (data[idx] < 0.0) data[idx] *= -1;
|
||||
|
||||
// Project on to the surface of the sphere
|
||||
// sp is the projected point ( dx, dy, dz ) * ( ra / d )
|
||||
const d = Math.sqrt(dSq);
|
||||
const ap = rad / d;
|
||||
const spx = dx * ap + vx;
|
||||
const spy = dy * ap + vy;
|
||||
const spz = dz * ap + vz;
|
||||
|
||||
if (obscured(spx, spy, spz, j, -1) === -1) {
|
||||
const dd = rad - d;
|
||||
if (dd < data[idx]) {
|
||||
data[idx] = dd;
|
||||
idData[idx] = id[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function projectPoints() {
|
||||
for (let i = 0; i < n; i += updateChunk) {
|
||||
projectPointsRange(i, Math.min(i + updateChunk, n));
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'projecting points', current: i, max: n });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vectors for Torus Projection
|
||||
const atob = Vec3();
|
||||
const mid = Vec3();
|
||||
const n1 = Vec3();
|
||||
const n2 = Vec3();
|
||||
/**
|
||||
* `a` and `b` must be resolved indices
|
||||
*/
|
||||
function projectTorus(a: number, b: number) {
|
||||
const rA = radius[a];
|
||||
const rB = radius[b];
|
||||
const dx = atob[0] = px[b] - px[a];
|
||||
const dy = atob[1] = py[b] - py[a];
|
||||
const dz = atob[2] = pz[b] - pz[a];
|
||||
const dSq = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
// This check now redundant as already done in AVHash.withinRadii
|
||||
// if (dSq > ((rA + rB) * (rA + rB))) { return }
|
||||
|
||||
const d = Math.sqrt(dSq);
|
||||
|
||||
// Find angle between a->b vector and the circle
|
||||
// of their intersection by cosine rule
|
||||
const cosA = (rA * rA + d * d - rB * rB) / (2.0 * rA * d);
|
||||
|
||||
// distance along a->b at intersection
|
||||
const dmp = rA * cosA;
|
||||
|
||||
Vec3.normalize(atob, atob);
|
||||
|
||||
// Create normal to line
|
||||
normalToLine(n1, atob);
|
||||
Vec3.normalize(n1, n1);
|
||||
|
||||
// Cross together for second normal vector
|
||||
Vec3.cross(n2, atob, n1);
|
||||
Vec3.normalize(n2, n2);
|
||||
|
||||
// r is radius of circle of intersection
|
||||
const rInt = Math.sqrt(rA * rA - dmp * dmp);
|
||||
|
||||
Vec3.scale(n1, n1, rInt);
|
||||
Vec3.scale(n2, n2, rInt);
|
||||
Vec3.scale(atob, atob, dmp);
|
||||
|
||||
mid[0] = atob[0] + px[a];
|
||||
mid[1] = atob[1] + py[a];
|
||||
mid[2] = atob[2] + pz[a];
|
||||
|
||||
lastClip = -1;
|
||||
|
||||
for (let i = 0; i < probePositions; ++i) {
|
||||
const cost = cosTable[i];
|
||||
const sint = sinTable[i];
|
||||
|
||||
const px = mid[0] + cost * n1[0] + sint * n2[0];
|
||||
const py = mid[1] + cost * n1[1] + sint * n2[1];
|
||||
const pz = mid[2] + cost * n1[2] + sint * n2[2];
|
||||
|
||||
if (obscured(px, py, pz, a, b) === -1) {
|
||||
const iax = Math.floor(scaleFactor * (px - minX));
|
||||
const iay = Math.floor(scaleFactor * (py - minY));
|
||||
const iaz = Math.floor(scaleFactor * (pz - minZ));
|
||||
|
||||
const begX = Math.max(0, iax - ngTorus);
|
||||
const begY = Math.max(0, iay - ngTorus);
|
||||
const begZ = Math.max(0, iaz - ngTorus);
|
||||
|
||||
const endX = Math.min(dimX, iax + ngTorus + 2);
|
||||
const endY = Math.min(dimY, iay + ngTorus + 2);
|
||||
const endZ = Math.min(dimZ, iaz + ngTorus + 2);
|
||||
|
||||
for (let xi = begX; xi < endX; ++xi) {
|
||||
const dx = px - gridx[xi];
|
||||
const xIdx = xi * iuv;
|
||||
|
||||
for (let yi = begY; yi < endY; ++yi) {
|
||||
const dy = py - gridy[yi];
|
||||
const dxySq = dx * dx + dy * dy;
|
||||
const xyIdx = yi * iu + xIdx;
|
||||
|
||||
for (let zi = begZ; zi < endZ; ++zi) {
|
||||
const dz = pz - gridz[zi];
|
||||
const dSq = dxySq + dz * dz;
|
||||
|
||||
const idx = zi + xyIdx;
|
||||
const current = data[idx];
|
||||
|
||||
if (current > 0.0 && dSq < (current * current)) {
|
||||
data[idx] = Math.sqrt(dSq);
|
||||
// Is this grid point closer to a or b?
|
||||
// Take dot product of atob and gridpoint->p (dx, dy, dz)
|
||||
const dp = dx * atob[0] + dy * atob[1] + dz * atob[2];
|
||||
idData[idx] = id[OrderedSet.indexOf(indices, dp < 0.0 ? b : a)];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function projectToriiRange(begI: number, endI: number) {
|
||||
for (let i = begI; i < endI; ++i) {
|
||||
const k = OrderedSet.getAt(indices, i);
|
||||
lookup3d.find(px[k], py[k], pz[k], radius[k]);
|
||||
for (let j = 0, jl = neighbours.count; j < jl; ++j) {
|
||||
const l = OrderedSet.getAt(indices, neighbours.indices[j]);
|
||||
if (k < l) projectTorus(k, l);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function projectTorii() {
|
||||
for (let i = 0; i < n; i += updateChunk) {
|
||||
projectToriiRange(i, Math.min(i + updateChunk, n));
|
||||
|
||||
if (ctx.shouldUpdate) {
|
||||
await ctx.update({ message: 'projecting torii', current: i, max: n });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.time('MolecularSurface')
|
||||
// console.time('MolecularSurface createState')
|
||||
const { resolution, probeRadius, probePositions } = surfaceData.props;
|
||||
const scaleFactor = 1 / resolution;
|
||||
const ngTorus = Math.max(5, 2 + Math.floor(probeRadius * scaleFactor));
|
||||
|
||||
const cellSize = Vec3.create(surfaceData.maxRadius, surfaceData.maxRadius, surfaceData.maxRadius);
|
||||
Vec3.scale(cellSize, cellSize, 2);
|
||||
const lookup3d = GridLookup3D(surfaceData.positions, surfaceData.boundary, cellSize);
|
||||
const neighbours = lookup3d.result;
|
||||
if (surfaceData.box === null) surfaceData.box = lookup3d.boundary.box;
|
||||
|
||||
const { indices, x: px, y: py, z: pz, id, radius } = surfaceData.positions;
|
||||
const n = OrderedSet.size(indices);
|
||||
|
||||
const pad = surfaceData.maxRadius + resolution;
|
||||
const expandedBox = Box3D.expand(Box3D(), surfaceData.box, Vec3.create(pad, pad, pad));
|
||||
const [minX, minY, minZ] = expandedBox.min;
|
||||
const scaledBox = Box3D.scale(Box3D(), expandedBox, scaleFactor);
|
||||
const dim = Box3D.size(Vec3(), scaledBox);
|
||||
Vec3.ceil(dim, dim);
|
||||
|
||||
const [dimX, dimY, dimZ] = dim;
|
||||
const iu = dimZ, iv = dimY, iuv = iu * iv;
|
||||
|
||||
const { cosTable, sinTable } = getAngleTables(probePositions);
|
||||
|
||||
const space = Tensor.Space(dim, [0, 1, 2], Float32Array);
|
||||
const data = space.create();
|
||||
const idData = space.create();
|
||||
|
||||
data.fill(-1001.0);
|
||||
idData.fill(-1);
|
||||
|
||||
const gridx = fillGridDim(dimX, minX, resolution);
|
||||
const gridy = fillGridDim(dimY, minY, resolution);
|
||||
const gridz = fillGridDim(dimZ, minZ, resolution);
|
||||
|
||||
const updateChunk = Math.ceil(100000 / ((Math.pow(Math.pow(surfaceData.maxRadius, 3), 3) * scaleFactor)));
|
||||
// console.timeEnd('MolecularSurface createState')
|
||||
|
||||
// console.time('MolecularSurface projectPoints')
|
||||
await projectPoints();
|
||||
// console.timeEnd('MolecularSurface projectPoints')
|
||||
|
||||
// console.time('MolecularSurface projectTorii')
|
||||
await projectTorii();
|
||||
// console.timeEnd('MolecularSurface projectTorii')
|
||||
// console.timeEnd('MolecularSurface')
|
||||
|
||||
const field = Tensor.create(space, data);
|
||||
const idField = Tensor.create(space, idData);
|
||||
|
||||
const transform = Mat4.identity();
|
||||
Mat4.fromScaling(transform, Vec3.create(resolution, resolution, resolution));
|
||||
Mat4.setTranslation(transform, expandedBox.min);
|
||||
// console.log({ field, idField, transform, updateChunk })
|
||||
return { field, idField, transform, resolution, maxRadius: surfaceData.maxRadius };
|
||||
}
|
||||
105
src/extensions/sb-ncbr/tunnels/behavior.ts
Normal file
105
src/extensions/sb-ncbr/tunnels/behavior.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginBehavior } from '../../../mol-plugin/behavior';
|
||||
import { DownloadTunnels } from './actions';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { PresetStructureRepresentations, StructureRepresentationPresetProvider } from '../../../mol-plugin-state/builder/structure/representation-preset';
|
||||
import { Model, Structure } from '../../../mol-model/structure';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { StateObjectRef } from '../../../mol-state';
|
||||
import { getTunnelsConfig, TunnelsDataParams } from './props';
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { Tunnel, ChannelsDBdata, TunnelDB } from './data-model';
|
||||
import { TunnelShapeProvider, TunnelFromRawData } from './representation';
|
||||
import { ColorGenerator } from '../../meshes/mesh-utils';
|
||||
|
||||
export const SbNcbrTunnels = PluginBehavior.create<{ autoAttach: boolean }>({
|
||||
name: 'sb-ncbr-tunnels',
|
||||
category: 'misc',
|
||||
display: {
|
||||
name: 'SB NCBR Tunnels',
|
||||
},
|
||||
ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean }> {
|
||||
register(): void {
|
||||
this.ctx.state.data.actions.add(DownloadTunnels);
|
||||
this.ctx.builders.structure.representation.registerPreset(TunnelsPreset);
|
||||
}
|
||||
unregister() {
|
||||
this.ctx.state.data.actions.remove(DownloadTunnels);
|
||||
this.ctx.builders.structure.representation.unregisterPreset(TunnelsPreset);
|
||||
}
|
||||
},
|
||||
params: () => ({
|
||||
autoAttach: PD.Boolean(true),
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
export function isApplicable(structure?: Structure): boolean {
|
||||
return (
|
||||
!!structure && structure.models.length === 1 &&
|
||||
Model.hasPdbId(structure.models[0])
|
||||
);
|
||||
}
|
||||
|
||||
export const TunnelsPreset = StructureRepresentationPresetProvider({
|
||||
id: 'sb-ncbr-preset-structure-tunnels',
|
||||
display: {
|
||||
name: 'Tunnels', group: 'Annotation',
|
||||
description: 'Shows Tunnels from ChannelsDB contained in the structure.'
|
||||
},
|
||||
isApplicable(a) {
|
||||
return isApplicable(a.data);
|
||||
},
|
||||
params: (a, plugin) => {
|
||||
return {
|
||||
...StructureRepresentationPresetProvider.CommonParams,
|
||||
...getConfiguredDefaultParams(plugin)
|
||||
};
|
||||
},
|
||||
async apply(ref, params, plugin) {
|
||||
const structureCell = StateObjectRef.resolveAndCheck(plugin.state.data, ref);
|
||||
const structure = structureCell?.obj?.data;
|
||||
if (!structureCell || !structure) return {};
|
||||
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
const response = await (await fetch(`${params.serverUrl}/channels/${params.serverType}/${structure.model.entryId.toLowerCase()}`)).json();
|
||||
const tunnels: Tunnel[] = [];
|
||||
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
await tunnels.forEach(async (tunnel) => {
|
||||
await update
|
||||
.toRoot()
|
||||
.apply(TunnelFromRawData, { data: tunnel })
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
colorTheme: ColorGenerator.next().value,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
await update.commit();
|
||||
});
|
||||
|
||||
const preset = await PresetStructureRepresentations.auto.apply(ref, { ...params }, plugin);
|
||||
|
||||
return { components: preset.components, representations: { ...preset.representations, } };
|
||||
}
|
||||
});
|
||||
|
||||
function getConfiguredDefaultParams(plugin: PluginContext) {
|
||||
const config = getTunnelsConfig(plugin);
|
||||
const params = PD.clone(TunnelsDataParams);
|
||||
PD.setDefaultValues(params, { serverType: config.DefaultServerType, serverUrl: config.DefaultServerUrl });
|
||||
return params;
|
||||
}
|
||||
119
src/extensions/sb-ncbr/tunnels/data-model.ts
Normal file
119
src/extensions/sb-ncbr/tunnels/data-model.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { WebGLContext } from '../../../mol-gl/webgl/context';
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { Color } from '../../../mol-util/color';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export interface Profile {
|
||||
Charge: number,
|
||||
Radius: number,
|
||||
FreeRadius: number,
|
||||
T: number,
|
||||
Distance: number,
|
||||
X: number,
|
||||
Y: number,
|
||||
Z: number
|
||||
}
|
||||
|
||||
export interface Layerweightedproperties {
|
||||
Hydrophobicity: number,
|
||||
Hydropathy: number,
|
||||
Polarity: number,
|
||||
Mutability: number
|
||||
}
|
||||
|
||||
export interface LayerGeometry {
|
||||
MinRadius: number,
|
||||
MinFreeRadius: number,
|
||||
StartDistance: number,
|
||||
EndDistance: number,
|
||||
LocalMinimum: boolean,
|
||||
Bottleneck: boolean,
|
||||
bottleneck: boolean
|
||||
}
|
||||
|
||||
export interface Properties {
|
||||
Charge: number,
|
||||
NumPositives: number,
|
||||
NumNegatives: number,
|
||||
Hydrophobicity: number,
|
||||
Hydropathy: number,
|
||||
Polarity: number,
|
||||
Mutability: number
|
||||
}
|
||||
|
||||
export interface LayersInfo {
|
||||
LayerGeometry: LayerGeometry,
|
||||
Residues: string[],
|
||||
FlowIndices: string[],
|
||||
Properties: Properties
|
||||
}
|
||||
|
||||
export interface Layers {
|
||||
ResidueFlow: string[],
|
||||
HetResidues: any[],
|
||||
LayerWeightedProperties: Layerweightedproperties
|
||||
LayersInfo: LayersInfo[]
|
||||
}
|
||||
|
||||
export interface TunnelDB {
|
||||
Type: string,
|
||||
Id: string,
|
||||
Cavity: string,
|
||||
Auto: boolean,
|
||||
Properties: Properties,
|
||||
Profile: Profile[],
|
||||
Layers: Layers
|
||||
};
|
||||
|
||||
export interface ChannelsDBdata {
|
||||
'CSATunnels_MOLE': TunnelDB[],
|
||||
'CSATunnels_Caver': TunnelDB[],
|
||||
'ReviewedChannels_MOLE': TunnelDB[],
|
||||
'ReviewedChannels_Caver': TunnelDB[],
|
||||
'CofactorTunnels_MOLE': TunnelDB[],
|
||||
'CofactorTunnels_Caver': TunnelDB[],
|
||||
'TransmembranePores_MOLE': TunnelDB[],
|
||||
'TransmembranePores_Caver': TunnelDB[],
|
||||
'ProcognateTunnels_MOLE': TunnelDB[],
|
||||
'ProcognateTunnels_Caver': TunnelDB[],
|
||||
'AlphaFillTunnels_MOLE': TunnelDB[],
|
||||
'AlphaFillTunnels_Caver': TunnelDB[]
|
||||
}
|
||||
|
||||
export interface ChannelsCache {
|
||||
Channels: ChannelsDBdata
|
||||
}
|
||||
|
||||
export interface Tunnel {
|
||||
data: Profile[],
|
||||
props: {
|
||||
highlight_label?: string,
|
||||
type?: string,
|
||||
id?: string,
|
||||
label?: string,
|
||||
description?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const TunnelShapeParams = {
|
||||
webgl: PD.Value<WebGLContext | null>(null),
|
||||
colorTheme: PD.Color(Color(0xff0000)),
|
||||
visual: PD.MappedStatic(
|
||||
'mesh',
|
||||
{
|
||||
mesh: PD.Group({ resolution: PD.Numeric(2) }),
|
||||
spheres: PD.Group({ resolution: PD.Numeric(2) })
|
||||
}
|
||||
),
|
||||
samplingRate: PD.Numeric(1, { min: 0.05, max: 1, step: 0.05 }),
|
||||
showRadii: PD.Boolean(false),
|
||||
};
|
||||
|
||||
export class TunnelStateObject extends PluginStateObject.Create<{ tunnel: Tunnel }>({ name: 'Tunnel Entry', typeClass: 'Data' }) { }
|
||||
export class TunnelsStateObject extends PluginStateObject.Create<{ tunnels: Tunnel[] }>({ name: 'Tunnels', typeClass: 'Data' }) { }
|
||||
63
src/extensions/sb-ncbr/tunnels/examples.ts
Normal file
63
src/extensions/sb-ncbr/tunnels/examples.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { StateTransforms } from '../../../mol-plugin-state/transforms';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ChannelsDBdata, Tunnel, TunnelDB } from './data-model';
|
||||
import { TunnelsFromRawData, SelectTunnel, TunnelShapeProvider, TunnelFromRawData } from './representation';
|
||||
|
||||
|
||||
export const DB_URL = 'https://channelsdb2.biodata.ceitec.cz/api/channels/';
|
||||
export const SUB_DB = 'pdb';
|
||||
export const CHANNEL = '1ymg';
|
||||
|
||||
export const URL = `${DB_URL}${SUB_DB}/${CHANNEL}`;
|
||||
|
||||
export async function runVisualizeTunnels(plugin: PluginContext, url: string = URL) {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
const response = await (await fetch(url)).json();
|
||||
|
||||
const tunnels: Tunnel[] = [];
|
||||
Object.entries(response.Channels as ChannelsDBdata).forEach(([key, values]) => {
|
||||
if (values.length > 0) {
|
||||
values.forEach((item: TunnelDB) => {
|
||||
tunnels.push({ data: item.Profile, props: { id: item.Id, type: item.Type } });
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelsFromRawData, { data: tunnels })
|
||||
.apply(SelectTunnel)
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
|
||||
export async function runVisualizeTunnel(plugin: PluginContext) {
|
||||
const update = plugin.build();
|
||||
const webgl = plugin.canvas3dContext?.webgl;
|
||||
|
||||
const response = await (await fetch(URL)).json();
|
||||
|
||||
const tunnel = response.Channels.TransmembranePores_MOLE[0];
|
||||
|
||||
update
|
||||
.toRoot()
|
||||
.apply(TunnelFromRawData, { data: { data: tunnel.Profile, props: { id: tunnel.Id, type: tunnel.Type } } })
|
||||
.apply(TunnelShapeProvider, {
|
||||
webgl,
|
||||
})
|
||||
.apply(StateTransforms.Representation.ShapeRepresentation3D);
|
||||
|
||||
await update.commit();
|
||||
}
|
||||
27
src/extensions/sb-ncbr/tunnels/props.ts
Normal file
27
src/extensions/sb-ncbr/tunnels/props.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { PluginConfigItem } from '../../../mol-plugin/config';
|
||||
import { PluginContext } from '../../../mol-plugin/context';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
|
||||
export namespace TunnelsData {
|
||||
export const DefaultServerUrl = 'https://channelsdb2.biodata.ceitec.cz/api';
|
||||
export const DefaultServerType = 'pdb';
|
||||
}
|
||||
|
||||
export const TunnelsDataParams = {
|
||||
serverType: PD.Select('pdb', [['pdb', 'pdb']] as const),
|
||||
serverUrl: PD.Text(TunnelsData.DefaultServerUrl)
|
||||
};
|
||||
export type TunnelsDataParams = typeof TunnelsDataParams
|
||||
|
||||
export const TunnelsServerConfig = {
|
||||
DefaultServerUrl: new PluginConfigItem('channelsdb-server', 'https://channelsdb2.biodata.ceitec.cz/api'),
|
||||
DefaultServerType: new PluginConfigItem<'pdb'>('serverType', 'pdb')
|
||||
};
|
||||
|
||||
export function getTunnelsConfig(plugin: PluginContext): { [key in keyof typeof TunnelsServerConfig]: NonNullable<typeof TunnelsServerConfig[key]['defaultValue']> } {
|
||||
return {
|
||||
DefaultServerUrl: plugin.config.get(TunnelsServerConfig.DefaultServerUrl) ?? TunnelsServerConfig.DefaultServerUrl.defaultValue ?? TunnelsData.DefaultServerUrl,
|
||||
DefaultServerType: plugin.config.get(TunnelsServerConfig.DefaultServerType) ?? TunnelsServerConfig.DefaultServerType.defaultValue ?? TunnelsData.DefaultServerType,
|
||||
};
|
||||
}
|
||||
|
||||
102
src/extensions/sb-ncbr/tunnels/representation.ts
Normal file
102
src/extensions/sb-ncbr/tunnels/representation.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Dušan Veľký <dvelky@mail.muni.cz>
|
||||
*/
|
||||
|
||||
import { PluginStateObject } from '../../../mol-plugin-state/objects';
|
||||
import { StateTransformer } from '../../../mol-state';
|
||||
import { TunnelStateObject, Tunnel, TunnelShapeParams, TunnelsStateObject } from './data-model';
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
|
||||
import { Task } from '../../../mol-task';
|
||||
import { createTunnelShape, createSpheresShape } from './algorithm';
|
||||
|
||||
const Transform = StateTransformer.builderFactory('sb-ncbr-tunnels');
|
||||
|
||||
export const TunnelsFromRawData = Transform({
|
||||
name: 'sb-ncbr-tunnels-from-data',
|
||||
display: { name: 'Tunnels' },
|
||||
from: PluginStateObject.Root,
|
||||
to: TunnelsStateObject,
|
||||
params: {
|
||||
data: PD.Value<Tunnel[]>([]),
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new TunnelsStateObject({ tunnels: params.data });
|
||||
},
|
||||
});
|
||||
|
||||
export const SelectTunnel = Transform({
|
||||
name: 'sb-ncbr-tunnel-from-tunnels',
|
||||
display: { name: 'Tunnel Selection' },
|
||||
from: TunnelsStateObject,
|
||||
to: TunnelStateObject,
|
||||
params: a => {
|
||||
return {
|
||||
index: PD.Numeric(0, { min: 0, max: a!.data.tunnels.length - 1, step: 1 })
|
||||
};
|
||||
}
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return new TunnelStateObject({ tunnel: a.data.tunnels[params.index] });
|
||||
}
|
||||
});
|
||||
|
||||
export const TunnelFromRawData = Transform({
|
||||
name: 'sb-ncbr-tunnel-from-data',
|
||||
display: { name: 'Tunnel Entry' },
|
||||
from: PluginStateObject.Root,
|
||||
to: TunnelStateObject,
|
||||
params: {
|
||||
data: PD.Value<Tunnel>(undefined as any, { isHidden: true })
|
||||
},
|
||||
})({
|
||||
apply({ params }) {
|
||||
return new TunnelStateObject({ tunnel: params.data });
|
||||
},
|
||||
});
|
||||
|
||||
export const TunnelShapeProvider = Transform({
|
||||
name: 'sb-ncbr-tunnel-shape-provider',
|
||||
display: { name: 'Tunnel' },
|
||||
from: TunnelStateObject,
|
||||
to: PluginStateObject.Shape.Provider,
|
||||
params: a => { return TunnelShapeParams; },
|
||||
})({
|
||||
apply({ a, params }) {
|
||||
return Task.create('Tunnel Shape Representation', async ctx => {
|
||||
return new PluginStateObject.Shape.Provider({
|
||||
label: 'Surface',
|
||||
data: { params, data: a.data },
|
||||
params: Mesh.Params,
|
||||
geometryUtils: Mesh.Utils,
|
||||
getShape: (_, data, __, mesh) => {
|
||||
if (data.params.visual.name === 'mesh' && !data.params.showRadii) {
|
||||
return createTunnelShape({
|
||||
tunnel: data.data.tunnel,
|
||||
color: data.params.colorTheme,
|
||||
resolution: data.params.visual.params.resolution,
|
||||
sampleRate: data.params.samplingRate,
|
||||
webgl: data.params.webgl, prev: mesh
|
||||
});
|
||||
}
|
||||
return createSpheresShape({
|
||||
tunnel: data.data.tunnel,
|
||||
color: data.params.colorTheme,
|
||||
resolution: data.params.visual.params.resolution,
|
||||
sampleRate: data.params.samplingRate,
|
||||
showRadii: data.params.showRadii, prev: mesh
|
||||
});
|
||||
}
|
||||
}, {
|
||||
label: a.data.tunnel.props.label ?? 'Tunnel',
|
||||
description: a.data.tunnel.props.description
|
||||
?? (a.data.tunnel.props.type && a.data.tunnel.props.id)
|
||||
? `${a.data.tunnel.props.type} ${a.data.tunnel.props.id}`
|
||||
: '',
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -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,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2024 Mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
*/
|
||||
@@ -87,22 +87,48 @@ class CameraTransitionManager {
|
||||
namespace CameraTransitionManager {
|
||||
export type TransitionFunc = (out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot) => void
|
||||
|
||||
const _rot = Quat.identity();
|
||||
const _rotUp = Quat.identity();
|
||||
const _rotDist = Quat.identity();
|
||||
|
||||
const _sourcePosition = Vec3();
|
||||
const _targetPosition = Vec3();
|
||||
|
||||
export function defaultTransition(out: Camera.Snapshot, t: number, source: Camera.Snapshot, target: Camera.Snapshot): void {
|
||||
Camera.copySnapshot(out, target);
|
||||
|
||||
// Rotate up
|
||||
Quat.slerp(_rot, Quat.Identity, Quat.rotationTo(_rot, source.up, target.up), t);
|
||||
Vec3.transformQuat(out.up, source.up, _rot);
|
||||
Quat.slerp(_rotUp, Quat.Identity, Quat.rotationTo(_rotUp, source.up, target.up), t);
|
||||
Vec3.transformQuat(out.up, source.up, _rotUp);
|
||||
|
||||
// Lerp target, position & radius
|
||||
Vec3.lerp(out.target, source.target, target.target, t);
|
||||
Vec3.lerp(out.position, source.position, target.position, t);
|
||||
|
||||
// Interpolate distance
|
||||
const distSource = Vec3.distance(source.target, source.position);
|
||||
const distTarget = Vec3.distance(target.target, target.position);
|
||||
const dist = lerp(distSource, distTarget, t);
|
||||
|
||||
// Rotate between source and targer direction
|
||||
Vec3.sub(_sourcePosition, source.position, source.target);
|
||||
Vec3.normalize(_sourcePosition, _sourcePosition);
|
||||
|
||||
Vec3.sub(_targetPosition, target.position, target.target);
|
||||
Vec3.normalize(_targetPosition, _targetPosition);
|
||||
|
||||
Quat.rotationTo(_rotDist, _sourcePosition, _targetPosition);
|
||||
Quat.slerp(_rotDist, Quat.Identity, _rotDist, t);
|
||||
|
||||
Vec3.transformQuat(_sourcePosition, _sourcePosition, _rotDist);
|
||||
Vec3.scale(_sourcePosition, _sourcePosition, dist);
|
||||
|
||||
Vec3.add(out.position, out.target, _sourcePosition);
|
||||
|
||||
// Interpolate radius
|
||||
out.radius = lerp(source.radius, target.radius, t);
|
||||
// TODO take change of `clipFar` into account
|
||||
out.radiusMax = lerp(source.radiusMax, target.radiusMax, t);
|
||||
|
||||
// Lerp fov & fog
|
||||
// Interpolate fov & fog
|
||||
out.fov = lerp(source.fov, target.fov, t);
|
||||
out.fog = lerp(source.fog, target.fog, t);
|
||||
}
|
||||
|
||||
@@ -278,7 +278,7 @@ interface Canvas3D {
|
||||
* Function for external "animation" control
|
||||
* Calls commit.
|
||||
*/
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }): void
|
||||
tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }): void
|
||||
update(repr?: Representation.Any, keepBoundingSphere?: boolean): void
|
||||
clear(): void
|
||||
syncVisibility(): void
|
||||
@@ -527,9 +527,15 @@ namespace Canvas3D {
|
||||
|
||||
let animationFrameHandle = 0;
|
||||
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean }) {
|
||||
function tick(t: now.Timestamp, options?: { isSynchronous?: boolean, manualDraw?: boolean, updateControls?: boolean }) {
|
||||
currentTime = t;
|
||||
commit(options?.isSynchronous);
|
||||
|
||||
// update the controler before the camera transition
|
||||
if (options?.updateControls) {
|
||||
controls.update(currentTime);
|
||||
}
|
||||
|
||||
camera.transition.tick(currentTime);
|
||||
hiZ.tick();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
212
src/mol-canvas3d/passes/dof.ts
Normal file
212
src/mol-canvas3d/passes/dof.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { TextureSpec, UniformSpec, DefineSpec, Values } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { Texture, createNullTexture } from '../../mol-gl/webgl/texture';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { dof_frag } from '../../mol-gl/shader/dof.frag';
|
||||
import { Viewport } from '../camera/util';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { Sphere3D } from '../../mol-math/geometry';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const DofParams = {
|
||||
blurSize: PD.Numeric(9, { min: 1, max: 32, step: 1 }),
|
||||
blurSpread: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
inFocus: PD.Numeric(0.0, { min: -5000.0, max: 5000.0, step: 1.0 }, { description: 'Distance from the scene center that will be in focus' }),
|
||||
PPM: PD.Numeric(20.0, { min: 0.0, max: 5000.0, step: 0.1 }, { description: 'Size of the area that will be in focus' }),
|
||||
center: PD.Select('camera-target', PD.arrayToOptions(['scene-center', 'camera-target'])),
|
||||
mode: PD.Select('plane', PD.arrayToOptions(['plane', 'sphere'])),
|
||||
};
|
||||
|
||||
export type DofProps = PD.Values<typeof DofParams>
|
||||
|
||||
export class DofPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.dof.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
private readonly renderable: DofRenderable;
|
||||
|
||||
constructor(private webgl: WebGLContext, width: number, height: number) {
|
||||
this.target = webgl.createRenderTarget(width, height, false);
|
||||
|
||||
const nullTexture = createNullTexture();
|
||||
this.renderable = getDofRenderable(webgl, nullTexture, nullTexture, nullTexture);
|
||||
}
|
||||
|
||||
private updateState(viewport: Viewport) {
|
||||
const { gl, state } = this.webgl;
|
||||
|
||||
state.enable(gl.SCISSOR_TEST);
|
||||
state.disable(gl.BLEND);
|
||||
state.disable(gl.DEPTH_TEST);
|
||||
state.depthMask(false);
|
||||
|
||||
const { x, y, width, height } = viewport;
|
||||
state.viewport(x, y, width, height);
|
||||
state.scissor(x, y, width, height);
|
||||
|
||||
state.clearColor(0, 0, 0, 1);
|
||||
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const w = this.target.texture.getWidth();
|
||||
const h = this.target.texture.getHeight();
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, input: Texture, depthOpaque: Texture, depthTransparent: Texture, props: DofProps, sphere: Sphere3D) {
|
||||
let needsUpdate = false;
|
||||
if (this.renderable.values.tColor.ref.value !== input) {
|
||||
ValueCell.update(this.renderable.values.tColor, input);
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (this.renderable.values.tDepthOpaque.ref.value !== depthOpaque) {
|
||||
ValueCell.update(this.renderable.values.tDepthOpaque, depthOpaque);
|
||||
needsUpdate = true;
|
||||
}
|
||||
if (this.renderable.values.tDepthTransparent.ref.value !== depthTransparent) {
|
||||
ValueCell.update(this.renderable.values.tDepthTransparent, depthTransparent);
|
||||
needsUpdate = true;
|
||||
}
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
const invProjection = this.renderable.values.uInvProjection.ref.value;
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
ValueCell.update(this.renderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.renderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.renderable.values.uMode, props.mode === 'sphere' ? 1 : 0);
|
||||
|
||||
Vec4.set(this.renderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.renderable.values.uBounds, this.renderable.values.uBounds.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.renderable.values.dOrthographic, orthographic);
|
||||
|
||||
const blurSize = Math.round(props.blurSize * this.webgl.pixelRatio);
|
||||
if (this.renderable.values.dBlurSize.ref.value !== blurSize) {
|
||||
ValueCell.update(this.renderable.values.dBlurSize, blurSize);
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
const wolrdCenter = (props.center === 'scene-center' ? sphere.center : camera.state.target);
|
||||
const distance = Vec3.distance(camera.state.position, wolrdCenter);
|
||||
const inFocus = distance + props.inFocus;
|
||||
ValueCell.updateIfChanged(this.renderable.values.uInFocus, inFocus);
|
||||
|
||||
// transform center in view space
|
||||
const center = this.renderable.values.uCenter.ref.value;
|
||||
Vec3.transformMat4(center, wolrdCenter, camera.view);
|
||||
ValueCell.update(this.renderable.values.uCenter, center);
|
||||
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBlurSpread, props.blurSpread);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uPPM, props.PPM);
|
||||
|
||||
if (needsUpdate) {
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(viewport: Viewport, target: undefined | RenderTarget) {
|
||||
if (isTimingMode) this.webgl.timer.mark('DofPass.render');
|
||||
if (target) {
|
||||
target.bind();
|
||||
} else {
|
||||
this.webgl.unbindFramebuffer();
|
||||
}
|
||||
this.updateState(viewport);
|
||||
this.renderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('DofPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
const DofSchema = {
|
||||
...QuadSchema,
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
uCenter: UniformSpec('v3'),
|
||||
uMode: UniformSpec('i'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
|
||||
dBlurSize: DefineSpec('number'),
|
||||
uBlurSpread: UniformSpec('f'),
|
||||
uInFocus: UniformSpec('f'),
|
||||
uPPM: UniformSpec('f'),
|
||||
};
|
||||
|
||||
const DofShaderCode = ShaderCode('dof', quad_vert, dof_frag);
|
||||
type DofRenderable = ComputeRenderable<Values<typeof DofSchema>>
|
||||
|
||||
function getDofRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture): DofRenderable {
|
||||
const width = colorTexture.getWidth();
|
||||
const height = colorTexture.getHeight();
|
||||
|
||||
const values: Values<typeof DofSchema> = {
|
||||
...QuadValues,
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
uCenter: ValueCell.create(Vec3()),
|
||||
uMode: ValueCell.create(0),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
|
||||
dBlurSize: ValueCell.create(5),
|
||||
uBlurSpread: ValueCell.create(300.0),
|
||||
uInFocus: ValueCell.create(20.0),
|
||||
uPPM: ValueCell.create(20.0),
|
||||
};
|
||||
|
||||
const schema = { ...DofSchema };
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', DofShaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { MarkingPass, MarkingProps } from './marking';
|
||||
import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
|
||||
import { isDebugMode, isTimingMode } from '../../mol-util/debug';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { DofPass } from './dof';
|
||||
import { BloomPass } from './bloom';
|
||||
|
||||
type Props = {
|
||||
@@ -63,6 +64,7 @@ export class DrawPass {
|
||||
readonly postprocessing: PostprocessingPass;
|
||||
readonly antialiasing: AntialiasingPass;
|
||||
readonly bloom: BloomPass;
|
||||
readonly dof: DofPass;
|
||||
|
||||
private transparencyMode: TransparencyMode = 'blended';
|
||||
setTransparency(transparency: 'wboit' | 'dpoit' | 'blended') {
|
||||
@@ -107,6 +109,7 @@ export class DrawPass {
|
||||
this.postprocessing = new PostprocessingPass(webgl, assetManager, this);
|
||||
this.antialiasing = new AntialiasingPass(webgl, width, height);
|
||||
this.bloom = new BloomPass(webgl, width, height);
|
||||
this.dof = new DofPass(webgl, width, height);
|
||||
|
||||
this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
|
||||
this.copyFboPostprocessing = createCopyRenderable(webgl, this.postprocessing.target.texture);
|
||||
@@ -148,6 +151,7 @@ export class DrawPass {
|
||||
this.marking.setSize(width, height);
|
||||
this.postprocessing.setSize(width, height);
|
||||
this.antialiasing.setSize(width, height);
|
||||
this.dof.setSize(width, height);
|
||||
this.bloom.setSize(width, height);
|
||||
}
|
||||
|
||||
@@ -162,15 +166,18 @@ export class DrawPass {
|
||||
renderer.renderDpoitOpaque(scene.primitives, camera, null);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
}
|
||||
|
||||
@@ -216,15 +223,18 @@ export class DrawPass {
|
||||
renderer.renderWboitOpaque(scene.primitives, camera, null);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
}
|
||||
|
||||
@@ -274,6 +284,17 @@ export class DrawPass {
|
||||
this.colorTarget.bind();
|
||||
}
|
||||
|
||||
const outlineEnabled = PostprocessingPass.isEnabled(postprocessingProps) && PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps);
|
||||
const dofEnabled = DofPass.isEnabled(postprocessingProps);
|
||||
|
||||
if (outlineEnabled || dofEnabled) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
if (!this.packedDepth) {
|
||||
this.depthTextureOpaque.detachFramebuffer(this.postprocessing.target.framebuffer, 'depth');
|
||||
@@ -281,14 +302,6 @@ export class DrawPass {
|
||||
this.colorTarget.depthRenderbuffer?.detachFramebuffer(this.postprocessing.target.framebuffer);
|
||||
}
|
||||
|
||||
if (PostprocessingPass.isTransparentOutlineEnabled(postprocessingProps)) {
|
||||
this.depthTargetTransparent.bind();
|
||||
renderer.clearDepth(true);
|
||||
if (scene.opacityAverage < 1) {
|
||||
renderer.renderDepthTransparent(scene.primitives, camera, this.depthTextureOpaque);
|
||||
}
|
||||
}
|
||||
|
||||
this.postprocessing.render(camera, false, transparentBackground, renderer.props.backgroundColor, postprocessingProps, renderer.light);
|
||||
|
||||
if (!this.packedDepth) {
|
||||
@@ -330,6 +343,7 @@ export class DrawPass {
|
||||
const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
|
||||
const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
|
||||
const markingEnabled = MarkingPass.isEnabled(props.marking);
|
||||
const dofEnabled = DofPass.isEnabled(props.postprocessing);
|
||||
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
renderer.setViewport(x, y, width, height);
|
||||
@@ -388,12 +402,30 @@ export class DrawPass {
|
||||
renderer.renderBlended(helper.camera.scene, helper.camera.camera);
|
||||
}
|
||||
|
||||
let needsTargetCopy = false;
|
||||
|
||||
if (antialiasingEnabled) {
|
||||
const input = PostprocessingPass.isEnabled(props.postprocessing)
|
||||
? this.postprocessing.target.texture
|
||||
: this.colorTarget.texture;
|
||||
this.antialiasing.render(camera, input, toDrawingBuffer, props.postprocessing);
|
||||
} else if (toDrawingBuffer) {
|
||||
this.antialiasing.render(camera, input, toDrawingBuffer && !dofEnabled, props.postprocessing);
|
||||
} else if (toDrawingBuffer && !DofPass.isEnabled(props.postprocessing)) {
|
||||
needsTargetCopy = true;
|
||||
}
|
||||
|
||||
if (props.postprocessing.dof.name === 'on') {
|
||||
const input = AntialiasingPass.isEnabled(props.postprocessing)
|
||||
? this.antialiasing.target.texture
|
||||
: PostprocessingPass.isEnabled(props.postprocessing)
|
||||
? this.postprocessing.target.texture
|
||||
: this.colorTarget.texture;
|
||||
this.dof.update(camera, input, this.depthTargetOpaque?.texture || this.depthTextureOpaque, this.depthTextureTransparent, props.postprocessing.dof.params, scene.boundingSphereVisible);
|
||||
this.dof.render(camera.viewport, toDrawingBuffer ? undefined : this.getColorTarget(props.postprocessing));
|
||||
} else if (toDrawingBuffer && !AntialiasingPass.isEnabled(props.postprocessing)) {
|
||||
needsTargetCopy = true;
|
||||
}
|
||||
|
||||
if (needsTargetCopy) {
|
||||
this.drawTarget.bind();
|
||||
|
||||
this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
|
||||
@@ -448,7 +480,9 @@ export class DrawPass {
|
||||
}
|
||||
|
||||
getColorTarget(postprocessingProps: PostprocessingProps): RenderTarget {
|
||||
if (AntialiasingPass.isEnabled(postprocessingProps)) {
|
||||
if (DofPass.isEnabled(postprocessingProps)) {
|
||||
return this.dof.target;
|
||||
} else if (AntialiasingPass.isEnabled(postprocessingProps)) {
|
||||
return this.antialiasing.target;
|
||||
} else if (PostprocessingPass.isEnabled(postprocessingProps)) {
|
||||
return this.postprocessing.target;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -19,6 +19,9 @@ import { Helper } from '../helper/helper';
|
||||
import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
|
||||
import { MarkingParams } from './marking';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
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),
|
||||
@@ -88,12 +91,13 @@ 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 (isTimingMode) this.webgl.timer.mark('ImagePass.render', true);
|
||||
if (MultiSamplePass.isEnabled(this.props.multiSample)) {
|
||||
this.multiSampleHelper.render(ctx, this.props, false);
|
||||
this._colorTarget = this.multiSamplePass.colorTarget;
|
||||
@@ -101,11 +105,21 @@ export class ImagePass {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -54,6 +54,7 @@ export const MultiSampleParams = {
|
||||
mode: PD.Select('temporal', [['off', 'Off'], ['on', 'On'], ['temporal', 'Temporal']]),
|
||||
sampleLevel: PD.Numeric(2, { min: 0, max: 5, step: 1 }, { description: 'Take level^2 samples.' }),
|
||||
reduceFlicker: PD.Boolean(true, { description: 'Reduce flicker in "temporal" mode.' }),
|
||||
reuseOcclusion: PD.Boolean(true, { description: 'Reuse occlusion data. It is faster but has some artefacts.' }),
|
||||
};
|
||||
export type MultiSampleProps = PD.Values<typeof MultiSampleParams>
|
||||
|
||||
@@ -161,7 +162,7 @@ export class MultiSamplePass {
|
||||
ValueCell.update(compose.values.uWeight, sampleWeight);
|
||||
|
||||
// render scene
|
||||
if (i === 0) {
|
||||
if (i === 0 || !props.multiSample.reuseOcclusion) {
|
||||
drawPass.postprocessing.setOcclusionOffset(0, 0);
|
||||
} else {
|
||||
drawPass.postprocessing.setOcclusionOffset(
|
||||
@@ -252,7 +253,7 @@ export class MultiSamplePass {
|
||||
camera.update();
|
||||
|
||||
// render scene
|
||||
if (sampleIndex === 0) {
|
||||
if (sampleIndex === 0 || !props.multiSample.reuseOcclusion) {
|
||||
drawPass.postprocessing.setOcclusionOffset(0, 0);
|
||||
} else {
|
||||
drawPass.postprocessing.setOcclusionOffset(
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -4,27 +4,24 @@
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { CopyRenderable, createCopyRenderable, QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { deepEqual, ValueCell } from '../../mol-util';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, Vec3, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { Vec2, Vec3 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { DrawPass } from './draw';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { outlines_frag } from '../../mol-gl/shader/outlines.frag';
|
||||
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
|
||||
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
|
||||
import { postprocessing_frag } from '../../mol-gl/shader/postprocessing.frag';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { FxaaParams, FxaaPass } from './fxaa';
|
||||
import { SmaaParams, SmaaPass } from './smaa';
|
||||
@@ -32,261 +29,19 @@ import { isTimingMode } from '../../mol-util/debug';
|
||||
import { BackgroundParams, BackgroundPass } from './background';
|
||||
import { AssetManager } from '../../mol-util/assets';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
|
||||
import { CasParams, CasPass } from './cas';
|
||||
import { DofParams } from './dof';
|
||||
import { BloomParams } from './bloom';
|
||||
import { OutlinePass, OutlineProps, OutlineParams } from './outline';
|
||||
import { ShadowPass, ShadowProps, ShadowParams } from './shadow';
|
||||
import { SsaoPass, SsaoProps, SsaoParams } from './ssao';
|
||||
|
||||
export const OutlinesSchema = {
|
||||
...QuadSchema,
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
|
||||
uOutlineThreshold: UniformSpec('f'),
|
||||
dTransparentOutline: DefineSpec('boolean'),
|
||||
};
|
||||
export type OutlinesRenderable = ComputeRenderable<Values<typeof OutlinesSchema>>
|
||||
|
||||
export function getOutlinesRenderable(ctx: WebGLContext, depthTextureOpaque: Texture, depthTextureTransparent: Texture, transparentOutline: boolean): OutlinesRenderable {
|
||||
const width = depthTextureOpaque.getWidth();
|
||||
const height = depthTextureOpaque.getHeight();
|
||||
|
||||
const values: Values<typeof OutlinesSchema> = {
|
||||
...QuadValues,
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
|
||||
uOutlineThreshold: ValueCell.create(0.33),
|
||||
dTransparentOutline: ValueCell.create(transparentOutline),
|
||||
};
|
||||
|
||||
const schema = { ...OutlinesSchema };
|
||||
const shaderCode = ShaderCode('outlines', quad_vert, outlines_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const ShadowsSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
dOrthographic: DefineSpec('number'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
|
||||
dSteps: DefineSpec('number'),
|
||||
uMaxDistance: UniformSpec('f'),
|
||||
uTolerance: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
uLightDirection: UniformSpec('v3[]'),
|
||||
uLightColor: UniformSpec('v3[]'),
|
||||
dLightCount: DefineSpec('number'),
|
||||
};
|
||||
type ShadowsRenderable = ComputeRenderable<Values<typeof ShadowsSchema>>
|
||||
|
||||
function getShadowsRenderable(ctx: WebGLContext, depthTexture: Texture): ShadowsRenderable {
|
||||
const width = depthTexture.getWidth();
|
||||
const height = depthTexture.getHeight();
|
||||
|
||||
const values: Values<typeof ShadowsSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(width, height)),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
dOrthographic: ValueCell.create(0),
|
||||
uNear: ValueCell.create(1),
|
||||
uFar: ValueCell.create(10000),
|
||||
|
||||
dSteps: ValueCell.create(1),
|
||||
uMaxDistance: ValueCell.create(3.0),
|
||||
uTolerance: ValueCell.create(1.0),
|
||||
uBias: ValueCell.create(0.6),
|
||||
|
||||
uLightDirection: ValueCell.create([]),
|
||||
uLightColor: ValueCell.create([]),
|
||||
dLightCount: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...ShadowsSchema };
|
||||
const shaderCode = ShaderCode('shadows', quad_vert, shadows_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
|
||||
uSamples: UniformSpec('v3[]'),
|
||||
dNSamples: DefineSpec('number'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uRadius: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
dMultiScale: DefineSpec('boolean'),
|
||||
dLevels: DefineSpec('number'),
|
||||
uLevelRadius: UniformSpec('f[]'),
|
||||
uLevelBias: UniformSpec('f[]'),
|
||||
uNearThreshold: UniformSpec('f'),
|
||||
uFarThreshold: UniformSpec('f'),
|
||||
};
|
||||
|
||||
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
|
||||
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
|
||||
const values: Values<typeof SsaoSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
tDepthHalf: ValueCell.create(depthHalfTexture),
|
||||
tDepthQuarter: ValueCell.create(depthQuarterTexture),
|
||||
|
||||
uSamples: ValueCell.create(getSamples(32)),
|
||||
dNSamples: ValueCell.create(32),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
uRadius: ValueCell.create(Math.pow(2, 5)),
|
||||
uBias: ValueCell.create(0.8),
|
||||
|
||||
dMultiScale: ValueCell.create(false),
|
||||
dLevels: ValueCell.create(3),
|
||||
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
|
||||
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
|
||||
uNearThreshold: ValueCell.create(10.0),
|
||||
uFarThreshold: ValueCell.create(1500.0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoSchema };
|
||||
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoBlurSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uKernel: UniformSpec('f[]'),
|
||||
dOcclusionKernelSize: DefineSpec('number'),
|
||||
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
dOrthographic: DefineSpec('number'),
|
||||
};
|
||||
|
||||
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
|
||||
|
||||
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
|
||||
const values: Values<typeof SsaoBlurSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
|
||||
|
||||
uKernel: ValueCell.create(getBlurKernel(15)),
|
||||
dOcclusionKernelSize: ValueCell.create(15),
|
||||
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
dOrthographic: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoBlurSchema };
|
||||
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function getBlurKernel(kernelSize: number): number[] {
|
||||
const sigma = kernelSize / 3.0;
|
||||
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
|
||||
|
||||
const kernel = [];
|
||||
for (let x = 0; x < halfKernelSize; x++) {
|
||||
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
const RandomHemisphereVector: Vec3[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const v = Vec3();
|
||||
v[0] = Math.random() * 2.0 - 1.0;
|
||||
v[1] = Math.random() * 2.0 - 1.0;
|
||||
v[2] = Math.random();
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, Math.random());
|
||||
RandomHemisphereVector.push(v);
|
||||
}
|
||||
|
||||
function getSamples(nSamples: number): number[] {
|
||||
const samples = [];
|
||||
for (let i = 0; i < nSamples; i++) {
|
||||
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
|
||||
scale = 0.1 + scale * (1.0 - 0.1);
|
||||
|
||||
samples.push(RandomHemisphereVector[i][0] * scale);
|
||||
samples.push(RandomHemisphereVector[i][1] * scale);
|
||||
samples.push(RandomHemisphereVector[i][2] * scale);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
|
||||
const PostprocessingSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthOpaque: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tDepthTransparent: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tShadows: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
tOutlines: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
@@ -312,13 +67,12 @@ const PostprocessingSchema = {
|
||||
};
|
||||
type PostprocessingRenderable = ComputeRenderable<Values<typeof PostprocessingSchema>>
|
||||
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, depthTextureTransparent: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
|
||||
function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, depthTextureOpaque: Texture, shadowsTexture: Texture, outlinesTexture: Texture, ssaoDepthTexture: Texture, transparentOutline: boolean): PostprocessingRenderable {
|
||||
const values: Values<typeof PostprocessingSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
tColor: ValueCell.create(colorTexture),
|
||||
tDepthOpaque: ValueCell.create(depthTextureOpaque),
|
||||
tDepthTransparent: ValueCell.create(depthTextureTransparent),
|
||||
tShadows: ValueCell.create(shadowsTexture),
|
||||
tOutlines: ValueCell.create(outlinesTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(colorTexture.getWidth(), colorTexture.getHeight())),
|
||||
@@ -352,50 +106,21 @@ function getPostprocessingRenderable(ctx: WebGLContext, colorTexture: Texture, d
|
||||
|
||||
export const PostprocessingParams = {
|
||||
occlusion: PD.MappedStatic('on', {
|
||||
on: PD.Group({
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
multiScale: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
levels: PD.ObjectList({
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
|
||||
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
|
||||
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
|
||||
{ radius: 2, bias: 1 },
|
||||
{ radius: 5, bias: 1 },
|
||||
{ radius: 8, bias: 1 },
|
||||
{ radius: 11, bias: 1 },
|
||||
] }),
|
||||
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
|
||||
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true }),
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
|
||||
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
|
||||
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
}),
|
||||
on: PD.Group(SsaoParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Darken occluded crevices with the ambient occlusion effect' }),
|
||||
shadow: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
|
||||
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
|
||||
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
}),
|
||||
on: PD.Group(ShadowParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Simplistic shadows' }),
|
||||
outline: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
scale: PD.Numeric(1, { min: 1, max: 5, step: 1 }),
|
||||
threshold: PD.Numeric(0.33, { min: 0.01, max: 1, step: 0.01 }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
includeTransparent: PD.Boolean(true, { description: 'Whether to show outline for transparent objects' }),
|
||||
}),
|
||||
on: PD.Group(OutlineParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'Draw outline around 3D objects' }),
|
||||
dof: PD.MappedStatic('off', {
|
||||
on: PD.Group(DofParams),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true, description: 'DOF' }),
|
||||
antialiasing: PD.MappedStatic('smaa', {
|
||||
fxaa: PD.Group(FxaaParams),
|
||||
smaa: PD.Group(SmaaParams),
|
||||
@@ -414,381 +139,76 @@ export const PostprocessingParams = {
|
||||
|
||||
export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
|
||||
|
||||
type Levels = {
|
||||
count: number
|
||||
radius: number[]
|
||||
bias: number[]
|
||||
}
|
||||
|
||||
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
|
||||
const count = props.length;
|
||||
const { radius, bias } = levels || {
|
||||
radius: (new Array(count * 3)).fill(0),
|
||||
bias: (new Array(count * 3)).fill(0),
|
||||
};
|
||||
props = props.slice().sort((a, b) => a.radius - b.radius);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
radius[i] = Math.pow(2, p.radius);
|
||||
bias[i] = p.bias;
|
||||
}
|
||||
return { count, radius, bias };
|
||||
}
|
||||
|
||||
export class PostprocessingPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.occlusion.name === 'on' || props.shadow.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
|
||||
return SsaoPass.isEnabled(props) || ShadowPass.isEnabled(props) || OutlinePass.isEnabled(props) || props.background.variant.name !== 'off';
|
||||
}
|
||||
|
||||
static isTransparentOutlineEnabled(props: PostprocessingProps) {
|
||||
return props.outline.name === 'on' && props.outline.params.includeTransparent;
|
||||
return OutlinePass.isEnabled(props) && (props.outline.params as OutlineProps).includeTransparent;
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
|
||||
private readonly outlinesTarget: RenderTarget;
|
||||
private readonly outlinesRenderable: OutlinesRenderable;
|
||||
|
||||
private readonly shadowsTarget: RenderTarget;
|
||||
private readonly shadowsRenderable: ShadowsRenderable;
|
||||
|
||||
private readonly ssaoFramebuffer: Framebuffer;
|
||||
private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
|
||||
private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
|
||||
|
||||
private readonly downsampledDepthTarget: RenderTarget;
|
||||
private readonly downsampleDepthRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTarget: RenderTarget;
|
||||
private readonly depthHalfRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthQuarterTarget: RenderTarget;
|
||||
private readonly depthQuarterRenderable: CopyRenderable;
|
||||
|
||||
private readonly ssaoDepthTexture: Texture;
|
||||
private readonly ssaoDepthBlurProxyTexture: Texture;
|
||||
|
||||
private readonly ssaoRenderable: SsaoRenderable;
|
||||
private readonly ssaoBlurFirstPassRenderable: SsaoBlurRenderable;
|
||||
private readonly ssaoBlurSecondPassRenderable: SsaoBlurRenderable;
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
|
||||
private readonly renderable: PostprocessingRenderable;
|
||||
|
||||
private ssaoScale: number;
|
||||
private calcSsaoScale(resolutionScale: number) {
|
||||
// downscale ssao for high pixel-ratios
|
||||
return Math.min(1, 1 / this.webgl.pixelRatio) * resolutionScale;
|
||||
}
|
||||
|
||||
private levels: { radius: number, bias: number }[];
|
||||
|
||||
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, 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);
|
||||
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, depthTextureOpaque, this.shadow.target.texture, this.outline.target.texture, this.ssao.ssaoDepthTexture, true);
|
||||
|
||||
this.background = new BackgroundPass(webgl, assetManager, width, height);
|
||||
}
|
||||
|
||||
setSize(width: number, height: number) {
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const ssaoScale = this.calcSsaoScale(1);
|
||||
|
||||
if (width !== w || height !== h || this.ssaoScale !== ssaoScale) {
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
if (width !== w || height !== h) {
|
||||
this.target.setSize(width, height);
|
||||
this.outlinesTarget.setSize(width, height);
|
||||
this.shadowsTarget.setSize(width, height);
|
||||
|
||||
const sw = Math.floor(width * this.ssaoScale);
|
||||
const sh = Math.floor(height * this.ssaoScale);
|
||||
this.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.max(1, Math.floor(sw * 0.5));
|
||||
const hh = Math.max(1, Math.floor(sh * 0.5));
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.outlinesRenderable.values.uTexSize, Vec2.set(this.outlinesRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.shadowsRenderable.values.uTexSize, Vec2.set(this.shadowsRenderable.values.uTexSize.ref.value, width, height));
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
|
||||
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
|
||||
|
||||
this.depthHalfRenderable.update();
|
||||
this.ssaoRenderable.update();
|
||||
|
||||
this.background.setSize(width, height);
|
||||
}
|
||||
|
||||
this.ssao.setSize(width, height);
|
||||
this.shadow.setSize(width, height);
|
||||
this.outline.setSize(width, height);
|
||||
this.background.setSize(width, height);
|
||||
}
|
||||
|
||||
updateState(camera: ICamera, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps, light: Light) {
|
||||
let needsUpdateShadows = false;
|
||||
let needsUpdateMain = false;
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
let needsUpdateOutlines = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
const outlinesEnabled = props.outline.name === 'on';
|
||||
const shadowsEnabled = props.shadow.name === 'on';
|
||||
const occlusionEnabled = props.occlusion.name === 'on';
|
||||
const outlinesEnabled = OutlinePass.isEnabled(props);
|
||||
const shadowsEnabled = ShadowPass.isEnabled(props);
|
||||
const occlusionEnabled = SsaoPass.isEnabled(props);
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.renderable.values.uTexSize.ref.value;
|
||||
const v = camera.viewport;
|
||||
|
||||
if (props.occlusion.name === 'on') {
|
||||
ValueCell.update(this.ssaoRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.ssaoRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
const b = this.ssaoRenderable.values.uBounds;
|
||||
const s = this.ssaoScale;
|
||||
Vec4.set(b.ref.value,
|
||||
Math.floor(v.x * s) / (w * s),
|
||||
Math.floor(v.y * s) / (h * s),
|
||||
Math.ceil((v.x + v.width) * s) / (w * s),
|
||||
Math.ceil((v.y + v.height) * s) / (h * s)
|
||||
);
|
||||
ValueCell.update(b, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uBounds, b.ref.value);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uBounds, b.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uNear, camera.near);
|
||||
|
||||
ValueCell.updateIfChanged(this.ssaoBlurFirstPassRenderable.values.uFar, camera.far);
|
||||
ValueCell.updateIfChanged(this.ssaoBlurSecondPassRenderable.values.uFar, camera.far);
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uInvProjection, invProjection);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
if (this.ssaoBlurFirstPassRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOrthographic, orthographic);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOrthographic, orthographic);
|
||||
}
|
||||
|
||||
if (this.nSamples !== props.occlusion.params.samples) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.nSamples = props.occlusion.params.samples;
|
||||
ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
|
||||
}
|
||||
|
||||
const multiScale = props.occlusion.params.multiScale.name === 'on';
|
||||
if (this.ssaoRenderable.values.dMultiScale.ref.value !== multiScale) {
|
||||
needsUpdateSsao = true;
|
||||
ValueCell.update(this.ssaoRenderable.values.dMultiScale, multiScale);
|
||||
}
|
||||
|
||||
if (props.occlusion.params.multiScale.name === 'on') {
|
||||
const mp = props.occlusion.params.multiScale.params;
|
||||
if (!deepEqual(this.levels, mp.levels)) {
|
||||
needsUpdateSsao = true;
|
||||
|
||||
this.levels = mp.levels;
|
||||
const levels = getLevels(mp.levels);
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.dLevels, levels.count);
|
||||
|
||||
ValueCell.update(this.ssaoRenderable.values.uLevelRadius, levels.radius);
|
||||
ValueCell.update(this.ssaoRenderable.values.uLevelBias, levels.bias);
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uNearThreshold, mp.nearThreshold);
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uFarThreshold, mp.farThreshold);
|
||||
} else {
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
|
||||
}
|
||||
ValueCell.updateIfChanged(this.ssaoRenderable.values.uBias, props.occlusion.params.bias);
|
||||
|
||||
if (this.blurKernelSize !== props.occlusion.params.blurKernelSize) {
|
||||
needsUpdateSsaoBlur = true;
|
||||
|
||||
this.blurKernelSize = props.occlusion.params.blurKernelSize;
|
||||
const kernel = getBlurKernel(this.blurKernelSize);
|
||||
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uKernel, kernel);
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.dOcclusionKernelSize, this.blurKernelSize);
|
||||
}
|
||||
|
||||
const ssaoScale = this.calcSsaoScale(props.occlusion.params.resolutionScale);
|
||||
if (this.ssaoScale !== ssaoScale) {
|
||||
needsUpdateSsao = true;
|
||||
needsUpdateDepthHalf = true;
|
||||
|
||||
this.ssaoScale = ssaoScale;
|
||||
|
||||
const sw = Math.floor(w * this.ssaoScale);
|
||||
const sh = Math.floor(h * this.ssaoScale);
|
||||
this.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.floor(sw * 0.5);
|
||||
const hh = Math.floor(sh * 0.5);
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.floor(sw * 0.25);
|
||||
const qh = Math.floor(sh * 0.25);
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
const depthTexture = this.ssaoScale === 1 ? this.drawPass.depthTextureOpaque : this.downsampledDepthTarget.texture;
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepth, depthTexture);
|
||||
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepthHalf, this.depthHalfTarget.texture);
|
||||
ValueCell.update(this.ssaoRenderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.ssaoRenderable.values.uTexSize, Vec2.set(this.ssaoRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurFirstPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.ssaoBlurSecondPassRenderable.values.uTexSize, Vec2.set(this.ssaoBlurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
}
|
||||
|
||||
ValueCell.update(this.renderable.values.uOcclusionColor, Color.toVec3Normalized(this.renderable.values.uOcclusionColor.ref.value, props.occlusion.params.color));
|
||||
if (occlusionEnabled) {
|
||||
this.ssao.update(camera, props.occlusion.params as SsaoProps);
|
||||
}
|
||||
|
||||
if (props.shadow.name === 'on') {
|
||||
ValueCell.update(this.shadowsRenderable.values.uProjection, camera.projection);
|
||||
ValueCell.update(this.shadowsRenderable.values.uInvProjection, invProjection);
|
||||
|
||||
Vec4.set(this.shadowsRenderable.values.uBounds.ref.value,
|
||||
v.x / w,
|
||||
v.y / h,
|
||||
(v.x + v.width) / w,
|
||||
(v.y + v.height) / h
|
||||
);
|
||||
ValueCell.update(this.shadowsRenderable.values.uBounds, this.shadowsRenderable.values.uBounds.ref.value);
|
||||
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uNear, camera.near);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uFar, camera.far);
|
||||
if (this.shadowsRenderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dOrthographic, orthographic);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uMaxDistance, props.shadow.params.maxDistance);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uTolerance, props.shadow.params.tolerance);
|
||||
ValueCell.updateIfChanged(this.shadowsRenderable.values.uBias, props.shadow.params.bias);
|
||||
if (this.shadowsRenderable.values.dSteps.ref.value !== props.shadow.params.steps) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dSteps, props.shadow.params.steps);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
|
||||
ValueCell.update(this.shadowsRenderable.values.uLightDirection, light.direction);
|
||||
ValueCell.update(this.shadowsRenderable.values.uLightColor, light.color);
|
||||
if (this.shadowsRenderable.values.dLightCount.ref.value !== light.count) {
|
||||
ValueCell.update(this.shadowsRenderable.values.dLightCount, light.count);
|
||||
needsUpdateShadows = true;
|
||||
}
|
||||
if (shadowsEnabled) {
|
||||
this.shadow.update(camera, light, props.shadow.params as ShadowProps);
|
||||
}
|
||||
|
||||
if (props.outline.name === 'on') {
|
||||
const transparentOutline = props.outline.params.includeTransparent ?? true;
|
||||
const outlineScale = Math.max(1, Math.round(props.outline.params.scale * this.webgl.pixelRatio)) - 1;
|
||||
const outlineThreshold = 50 * props.outline.params.threshold * this.webgl.pixelRatio;
|
||||
if (outlinesEnabled) {
|
||||
const outlineProps = props.outline.params as OutlineProps;
|
||||
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;
|
||||
@@ -806,6 +226,7 @@ export class PostprocessingPass {
|
||||
ValueCell.updateIfChanged(this.renderable.values.uFogNear, camera.fogNear);
|
||||
ValueCell.update(this.renderable.values.uFogColor, Color.toVec3Normalized(this.renderable.values.uFogColor.ref.value, backgroundColor));
|
||||
ValueCell.updateIfChanged(this.renderable.values.uTransparentBackground, transparentBackground);
|
||||
|
||||
if (this.renderable.values.dOrthographic.ref.value !== orthographic) {
|
||||
needsUpdateMain = true;
|
||||
ValueCell.update(this.renderable.values.dOrthographic, orthographic);
|
||||
@@ -824,27 +245,6 @@ export class PostprocessingPass {
|
||||
ValueCell.update(this.renderable.values.dOcclusionEnable, occlusionEnabled);
|
||||
}
|
||||
|
||||
if (needsUpdateOutlines) {
|
||||
this.outlinesRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateShadows) {
|
||||
this.shadowsRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsao) {
|
||||
this.ssaoRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsaoBlur) {
|
||||
this.ssaoBlurFirstPassRenderable.update();
|
||||
this.ssaoBlurSecondPassRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateMain) {
|
||||
this.renderable.update();
|
||||
}
|
||||
@@ -873,60 +273,24 @@ export class PostprocessingPass {
|
||||
if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
|
||||
this.updateState(camera, transparentBackground, backgroundColor, props, light);
|
||||
|
||||
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) {
|
||||
@@ -936,21 +300,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');
|
||||
|
||||
172
src/mol-canvas3d/passes/shadow.ts
Normal file
172
src/mol-canvas3d/passes/shadow.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
|
||||
* @author Ludovic Autin <ludovic.autin@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
|
||||
import { TextureSpec, Values, UniformSpec, DefineSpec } from '../../mol-gl/renderable/schema';
|
||||
import { ShaderCode } from '../../mol-gl/shader-code';
|
||||
import { WebGLContext } from '../../mol-gl/webgl/context';
|
||||
import { Texture } from '../../mol-gl/webgl/texture';
|
||||
import { ValueCell } from '../../mol-util';
|
||||
import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
|
||||
import { createComputeRenderable, ComputeRenderable } from '../../mol-gl/renderable';
|
||||
import { Mat4, Vec2, Vec4 } from '../../mol-math/linear-algebra';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { RenderTarget } from '../../mol-gl/webgl/render-target';
|
||||
import { ICamera } from '../../mol-canvas3d/camera';
|
||||
import { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { Light } from '../../mol-gl/renderer';
|
||||
import { shadows_frag } from '../../mol-gl/shader/shadows.frag';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const ShadowParams = {
|
||||
steps: PD.Numeric(1, { min: 1, max: 64, step: 1 }),
|
||||
bias: PD.Numeric(0.6, { min: 0.0, max: 1.0, step: 0.01 }),
|
||||
maxDistance: PD.Numeric(3, { min: 0, max: 256, step: 1 }),
|
||||
tolerance: PD.Numeric(1.0, { min: 0.0, max: 10.0, step: 0.1 }),
|
||||
};
|
||||
|
||||
export type ShadowProps = PD.Values<typeof ShadowParams>
|
||||
|
||||
export class ShadowPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.shadow.name !== 'off';
|
||||
}
|
||||
|
||||
readonly 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, 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);
|
||||
ValueCell.updateIfChanged(this.renderable.values.uBias, props.bias);
|
||||
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;
|
||||
}
|
||||
|
||||
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'),
|
||||
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);
|
||||
}
|
||||
545
src/mol-canvas3d/passes/ssao.ts
Normal file
545
src/mol-canvas3d/passes/ssao.ts
Normal file
@@ -0,0 +1,545 @@
|
||||
/**
|
||||
* 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 { quad_vert } from '../../mol-gl/shader/quad.vert';
|
||||
import { ssao_frag } from '../../mol-gl/shader/ssao.frag';
|
||||
import { ssaoBlur_frag } from '../../mol-gl/shader/ssao-blur.frag';
|
||||
import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
|
||||
import { Color } from '../../mol-util/color';
|
||||
import { isTimingMode } from '../../mol-util/debug';
|
||||
import { PostprocessingProps } from './postprocessing';
|
||||
|
||||
export const SsaoParams = {
|
||||
samples: PD.Numeric(32, { min: 1, max: 256, step: 1 }),
|
||||
multiScale: PD.MappedStatic('off', {
|
||||
on: PD.Group({
|
||||
levels: PD.ObjectList({
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x' }),
|
||||
bias: PD.Numeric(1, { min: 0, max: 3, step: 0.1 }),
|
||||
}, o => `${o.radius}, ${o.bias}`, { defaultValue: [
|
||||
{ radius: 2, bias: 1 },
|
||||
{ radius: 5, bias: 1 },
|
||||
{ radius: 8, bias: 1 },
|
||||
{ radius: 11, bias: 1 },
|
||||
] }),
|
||||
nearThreshold: PD.Numeric(10, { min: 0, max: 50, step: 1 }),
|
||||
farThreshold: PD.Numeric(1500, { min: 0, max: 10000, step: 100 }),
|
||||
}),
|
||||
off: PD.Group({})
|
||||
}, { cycle: true }),
|
||||
radius: PD.Numeric(5, { min: 0, max: 20, step: 0.1 }, { description: 'Final occlusion radius is 2^x', hideIf: p => p?.multiScale.name === 'on' }),
|
||||
bias: PD.Numeric(0.8, { min: 0, max: 3, step: 0.1 }),
|
||||
blurKernelSize: PD.Numeric(15, { min: 1, max: 25, step: 2 }),
|
||||
blurDepthBias: PD.Numeric(0.5, { min: 0, max: 1, step: 0.01 }),
|
||||
resolutionScale: PD.Numeric(1, { min: 0.1, max: 1, step: 0.05 }, { description: 'Adjust resolution of occlusion calculation' }),
|
||||
color: PD.Color(Color(0x000000)),
|
||||
};
|
||||
|
||||
export type SsaoProps = PD.Values<typeof SsaoParams>
|
||||
|
||||
type Levels = {
|
||||
count: number
|
||||
radius: number[]
|
||||
bias: number[]
|
||||
}
|
||||
|
||||
function getLevels(props: { radius: number, bias: number }[], levels?: Levels): Levels {
|
||||
const count = props.length;
|
||||
const { radius, bias } = levels || {
|
||||
radius: (new Array(count * 3)).fill(0),
|
||||
bias: (new Array(count * 3)).fill(0),
|
||||
};
|
||||
props = props.slice().sort((a, b) => a.radius - b.radius);
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const p = props[i];
|
||||
radius[i] = Math.pow(2, p.radius);
|
||||
bias[i] = p.bias;
|
||||
}
|
||||
return { count, radius, bias };
|
||||
}
|
||||
|
||||
export class SsaoPass {
|
||||
static isEnabled(props: PostprocessingProps) {
|
||||
return props.occlusion.name !== 'off';
|
||||
}
|
||||
|
||||
readonly target: RenderTarget;
|
||||
|
||||
private readonly framebuffer: Framebuffer;
|
||||
private readonly blurFirstPassFramebuffer: Framebuffer;
|
||||
private readonly blurSecondPassFramebuffer: Framebuffer;
|
||||
|
||||
private readonly downsampledDepthTarget: RenderTarget;
|
||||
private readonly downsampleDepthRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthHalfTarget: RenderTarget;
|
||||
private readonly depthHalfRenderable: CopyRenderable;
|
||||
|
||||
private readonly depthQuarterTarget: RenderTarget;
|
||||
private readonly depthQuarterRenderable: CopyRenderable;
|
||||
|
||||
readonly ssaoDepthTexture: Texture;
|
||||
private readonly depthBlurProxyTexture: Texture;
|
||||
|
||||
private readonly renderable: SsaoRenderable;
|
||||
private readonly blurFirstPassRenderable: SsaoBlurRenderable;
|
||||
private readonly blurSecondPassRenderable: SsaoBlurRenderable;
|
||||
|
||||
private depthTexture: Texture;
|
||||
|
||||
private nSamples: number;
|
||||
private blurKernelSize: number;
|
||||
private texSize: [number, number];
|
||||
|
||||
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.depthTexture : this.downsampledDepthTarget.texture;
|
||||
}
|
||||
|
||||
constructor(private readonly webgl: WebGLContext, width: number, height: number, packedDepth: boolean, depthTexture: Texture) {
|
||||
const { textureFloatLinear } = webgl.extensions;
|
||||
|
||||
this.depthTexture = depthTexture;
|
||||
|
||||
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.downsampledDepthTarget = packedDepth
|
||||
? webgl.createRenderTarget(sw, sh, false, 'uint8', filter, 'rgba')
|
||||
: webgl.createRenderTarget(sw, sh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.downsampleDepthRenderable = createCopyRenderable(webgl, depthTexture);
|
||||
|
||||
this.depthHalfTarget = packedDepth
|
||||
? webgl.createRenderTarget(hw, hh, false, 'uint8', filter, 'rgba')
|
||||
: webgl.createRenderTarget(hw, hh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthHalfRenderable = createCopyRenderable(webgl, this.getDepthTexture());
|
||||
|
||||
this.depthQuarterTarget = packedDepth
|
||||
? webgl.createRenderTarget(qw, qh, false, 'uint8', filter, 'rgba')
|
||||
: webgl.createRenderTarget(qw, qh, false, 'float32', filter, webgl.isWebGL2 ? 'alpha' : 'rgba');
|
||||
this.depthQuarterRenderable = createCopyRenderable(webgl, this.depthHalfTarget.texture);
|
||||
|
||||
this.ssaoDepthTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', filter);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.framebuffer, 'color0');
|
||||
|
||||
this.depthBlurProxyTexture = webgl.resources.texture('image-uint8', 'rgba', 'ubyte', filter);
|
||||
this.depthBlurProxyTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.attachFramebuffer(this.blurFirstPassFramebuffer, 'color0');
|
||||
|
||||
this.ssaoDepthTexture.attachFramebuffer(this.blurSecondPassFramebuffer, 'color0');
|
||||
|
||||
this.renderable = getSsaoRenderable(webgl, this.getDepthTexture(), this.depthHalfTarget.texture, this.depthQuarterTarget.texture);
|
||||
this.blurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
|
||||
this.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.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.max(1, Math.floor(sw * 0.5));
|
||||
const hh = Math.max(1, Math.floor(sh * 0.5));
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.max(1, Math.floor(sw * 0.25));
|
||||
const qh = Math.max(1, Math.floor(sh * 0.25));
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepth, depthTexture);
|
||||
|
||||
this.depthHalfRenderable.update();
|
||||
this.renderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
update(camera: ICamera, props: SsaoProps) {
|
||||
let needsUpdateSsao = false;
|
||||
let needsUpdateSsaoBlur = false;
|
||||
let needsUpdateDepthHalf = false;
|
||||
|
||||
const orthographic = camera.state.mode === 'orthographic' ? 1 : 0;
|
||||
|
||||
const invProjection = Mat4.identity();
|
||||
Mat4.invert(invProjection, camera.projection);
|
||||
|
||||
const [w, h] = this.texSize;
|
||||
const v = camera.viewport;
|
||||
|
||||
ValueCell.update(this.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);
|
||||
}
|
||||
|
||||
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.downsampledDepthTarget.setSize(sw, sh);
|
||||
this.ssaoDepthTexture.define(sw, sh);
|
||||
this.depthBlurProxyTexture.define(sw, sh);
|
||||
|
||||
const hw = Math.floor(sw * 0.5);
|
||||
const hh = Math.floor(sh * 0.5);
|
||||
this.depthHalfTarget.setSize(hw, hh);
|
||||
|
||||
const qw = Math.floor(sw * 0.25);
|
||||
const qh = Math.floor(sh * 0.25);
|
||||
this.depthQuarterTarget.setSize(qw, qh);
|
||||
|
||||
const depthTexture = this.getDepthTexture();
|
||||
ValueCell.update(this.depthHalfRenderable.values.tColor, depthTexture);
|
||||
ValueCell.update(this.renderable.values.tDepth, depthTexture);
|
||||
|
||||
ValueCell.update(this.renderable.values.tDepthHalf, this.depthHalfTarget.texture);
|
||||
ValueCell.update(this.renderable.values.tDepthQuarter, this.depthQuarterTarget.texture);
|
||||
|
||||
ValueCell.update(this.downsampleDepthRenderable.values.uTexSize, Vec2.set(this.downsampleDepthRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.depthHalfRenderable.values.uTexSize, Vec2.set(this.depthHalfRenderable.values.uTexSize.ref.value, hw, hh));
|
||||
ValueCell.update(this.depthQuarterRenderable.values.uTexSize, Vec2.set(this.depthQuarterRenderable.values.uTexSize.ref.value, qw, qh));
|
||||
ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurFirstPassRenderable.values.uTexSize, Vec2.set(this.blurFirstPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
ValueCell.update(this.blurSecondPassRenderable.values.uTexSize, Vec2.set(this.blurSecondPassRenderable.values.uTexSize.ref.value, sw, sh));
|
||||
}
|
||||
|
||||
if (needsUpdateSsao) {
|
||||
this.renderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateSsaoBlur) {
|
||||
this.blurFirstPassRenderable.update();
|
||||
this.blurSecondPassRenderable.update();
|
||||
}
|
||||
|
||||
if (needsUpdateDepthHalf) {
|
||||
this.depthHalfRenderable.update();
|
||||
}
|
||||
}
|
||||
|
||||
render(camera: ICamera) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.render');
|
||||
|
||||
const { state } = this.webgl;
|
||||
const { x, y, width, height } = camera.viewport;
|
||||
|
||||
const sx = Math.floor(x * this.ssaoScale);
|
||||
const sy = Math.floor(y * this.ssaoScale);
|
||||
const sw = Math.ceil(width * this.ssaoScale);
|
||||
const sh = Math.ceil(height * this.ssaoScale);
|
||||
|
||||
state.viewport(sx, sy, sw, sh);
|
||||
state.scissor(sx, sy, sw, sh);
|
||||
|
||||
if (this.ssaoScale < 1) {
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.downsample');
|
||||
this.downsampledDepthTarget.bind();
|
||||
this.downsampleDepthRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.downsample');
|
||||
}
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.half');
|
||||
this.depthHalfTarget.bind();
|
||||
this.depthHalfRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.half');
|
||||
|
||||
if (isTimingMode) this.webgl.timer.mark('SsaoPass.quarter');
|
||||
this.depthQuarterTarget.bind();
|
||||
this.depthQuarterRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.quarter');
|
||||
|
||||
this.framebuffer.bind();
|
||||
this.renderable.render();
|
||||
|
||||
this.blurFirstPassFramebuffer.bind();
|
||||
this.blurFirstPassRenderable.render();
|
||||
|
||||
this.blurSecondPassFramebuffer.bind();
|
||||
this.blurSecondPassRenderable.render();
|
||||
if (isTimingMode) this.webgl.timer.markEnd('SsaoPass.render');
|
||||
}
|
||||
}
|
||||
|
||||
const SsaoSchema = {
|
||||
...QuadSchema,
|
||||
tDepth: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
tDepthHalf: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
tDepthQuarter: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
|
||||
|
||||
uSamples: UniformSpec('v3[]'),
|
||||
dNSamples: DefineSpec('number'),
|
||||
|
||||
uProjection: UniformSpec('m4'),
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uRadius: UniformSpec('f'),
|
||||
uBias: UniformSpec('f'),
|
||||
|
||||
dMultiScale: DefineSpec('boolean'),
|
||||
dLevels: DefineSpec('number'),
|
||||
uLevelRadius: UniformSpec('f[]'),
|
||||
uLevelBias: UniformSpec('f[]'),
|
||||
uNearThreshold: UniformSpec('f'),
|
||||
uFarThreshold: UniformSpec('f'),
|
||||
};
|
||||
|
||||
type SsaoRenderable = ComputeRenderable<Values<typeof SsaoSchema>>
|
||||
|
||||
function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture, depthHalfTexture: Texture, depthQuarterTexture: Texture): SsaoRenderable {
|
||||
const values: Values<typeof SsaoSchema> = {
|
||||
...QuadValues,
|
||||
tDepth: ValueCell.create(depthTexture),
|
||||
tDepthHalf: ValueCell.create(depthHalfTexture),
|
||||
tDepthQuarter: ValueCell.create(depthQuarterTexture),
|
||||
|
||||
uSamples: ValueCell.create(getSamples(32)),
|
||||
dNSamples: ValueCell.create(32),
|
||||
|
||||
uProjection: ValueCell.create(Mat4.identity()),
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
|
||||
uTexSize: ValueCell.create(Vec2.create(ctx.gl.drawingBufferWidth, ctx.gl.drawingBufferHeight)),
|
||||
|
||||
uRadius: ValueCell.create(Math.pow(2, 5)),
|
||||
uBias: ValueCell.create(0.8),
|
||||
|
||||
dMultiScale: ValueCell.create(false),
|
||||
dLevels: ValueCell.create(3),
|
||||
uLevelRadius: ValueCell.create([Math.pow(2, 2), Math.pow(2, 5), Math.pow(2, 8)]),
|
||||
uLevelBias: ValueCell.create([0.8, 0.8, 0.8]),
|
||||
uNearThreshold: ValueCell.create(10.0),
|
||||
uFarThreshold: ValueCell.create(1500.0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoSchema };
|
||||
const shaderCode = ShaderCode('ssao', quad_vert, ssao_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
const SsaoBlurSchema = {
|
||||
...QuadSchema,
|
||||
tSsaoDepth: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
|
||||
uTexSize: UniformSpec('v2'),
|
||||
|
||||
uKernel: UniformSpec('f[]'),
|
||||
dOcclusionKernelSize: DefineSpec('number'),
|
||||
uBlurDepthBias: UniformSpec('f'),
|
||||
|
||||
uBlurDirectionX: UniformSpec('f'),
|
||||
uBlurDirectionY: UniformSpec('f'),
|
||||
|
||||
uInvProjection: UniformSpec('m4'),
|
||||
uNear: UniformSpec('f'),
|
||||
uFar: UniformSpec('f'),
|
||||
uBounds: UniformSpec('v4'),
|
||||
dOrthographic: DefineSpec('number'),
|
||||
};
|
||||
|
||||
type SsaoBlurRenderable = ComputeRenderable<Values<typeof SsaoBlurSchema>>
|
||||
|
||||
function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, direction: 'horizontal' | 'vertical'): SsaoBlurRenderable {
|
||||
const values: Values<typeof SsaoBlurSchema> = {
|
||||
...QuadValues,
|
||||
tSsaoDepth: ValueCell.create(ssaoDepthTexture),
|
||||
uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
|
||||
|
||||
uKernel: ValueCell.create(getBlurKernel(15)),
|
||||
dOcclusionKernelSize: ValueCell.create(15),
|
||||
uBlurDepthBias: ValueCell.create(0.5),
|
||||
|
||||
uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
|
||||
uBlurDirectionY: ValueCell.create(direction === 'vertical' ? 1 : 0),
|
||||
|
||||
uInvProjection: ValueCell.create(Mat4.identity()),
|
||||
uNear: ValueCell.create(0.0),
|
||||
uFar: ValueCell.create(10000.0),
|
||||
uBounds: ValueCell.create(Vec4()),
|
||||
dOrthographic: ValueCell.create(0),
|
||||
};
|
||||
|
||||
const schema = { ...SsaoBlurSchema };
|
||||
const shaderCode = ShaderCode('ssao_blur', quad_vert, ssaoBlur_frag);
|
||||
const renderItem = createComputeRenderItem(ctx, 'triangles', shaderCode, schema, values);
|
||||
|
||||
return createComputeRenderable(renderItem, values);
|
||||
}
|
||||
|
||||
function getBlurKernel(kernelSize: number): number[] {
|
||||
const sigma = kernelSize / 3.0;
|
||||
const halfKernelSize = Math.floor((kernelSize + 1) / 2);
|
||||
|
||||
const kernel = [];
|
||||
for (let x = 0; x < halfKernelSize; x++) {
|
||||
kernel.push((1.0 / ((Math.sqrt(2 * Math.PI)) * sigma)) * Math.exp(-x * x / (2 * sigma * sigma)));
|
||||
}
|
||||
|
||||
return kernel;
|
||||
}
|
||||
|
||||
const RandomHemisphereVector: Vec3[] = [];
|
||||
for (let i = 0; i < 256; i++) {
|
||||
const v = Vec3();
|
||||
v[0] = Math.random() * 2.0 - 1.0;
|
||||
v[1] = Math.random() * 2.0 - 1.0;
|
||||
v[2] = Math.random();
|
||||
Vec3.normalize(v, v);
|
||||
Vec3.scale(v, v, Math.random());
|
||||
RandomHemisphereVector.push(v);
|
||||
}
|
||||
|
||||
function getSamples(nSamples: number): number[] {
|
||||
const samples = [];
|
||||
for (let i = 0; i < nSamples; i++) {
|
||||
let scale = (i * i + 2.0 * i + 1) / (nSamples * nSamples);
|
||||
scale = 0.1 + scale * (1.0 - 0.1);
|
||||
|
||||
samples.push(RandomHemisphereVector[i][0] * scale);
|
||||
samples.push(RandomHemisphereVector[i][1] * scale);
|
||||
samples.push(RandomHemisphereVector[i][2] * scale);
|
||||
}
|
||||
|
||||
return samples;
|
||||
}
|
||||
@@ -168,6 +168,7 @@ export namespace Cylinders {
|
||||
sizeAspectRatio: PD.Numeric(1, { min: 0, max: 3, step: 0.01 }),
|
||||
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
|
||||
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
|
||||
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
|
||||
@@ -259,6 +260,7 @@ export namespace Cylinders {
|
||||
uSizeFactor: ValueCell.create(props.sizeFactor * props.sizeAspectRatio),
|
||||
uDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dCelShaded: ValueCell.create(props.celShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
@@ -279,6 +281,7 @@ export namespace Cylinders {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor * props.sizeAspectRatio);
|
||||
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
|
||||
|
||||
@@ -148,6 +148,7 @@ export namespace DirectVolume {
|
||||
export const Params = {
|
||||
...BaseGeometry.Params,
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
|
||||
controlPoints: PD.LineGraph([
|
||||
Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
|
||||
@@ -272,6 +273,7 @@ export namespace DirectVolume {
|
||||
dAxisOrder: ValueCell.create(directVolume.axisOrder.ref.value.join('')),
|
||||
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dCelShaded: ValueCell.create(props.celShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
};
|
||||
}
|
||||
@@ -285,6 +287,7 @@ export namespace DirectVolume {
|
||||
function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
|
||||
BaseGeometry.updateValues(values, props);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
|
||||
const controlPoints = getControlPointsFromVec2Array(props.controlPoints);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2018-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
import { Vec3 } from '../../../../mol-math/linear-algebra';
|
||||
@@ -10,6 +11,7 @@ import { cantorPairing, ChunkedArray } from '../../../../mol-data/util';
|
||||
import { MeshBuilder } from '../mesh-builder';
|
||||
|
||||
const normalVector = Vec3();
|
||||
const capNormalSmoothingVector = Vec3();
|
||||
const surfacePoint = Vec3();
|
||||
const controlPoint = Vec3();
|
||||
const u = Vec3();
|
||||
@@ -32,6 +34,7 @@ const v3fromArray = Vec3.fromArray;
|
||||
const v3normalize = Vec3.normalize;
|
||||
const v3scaleAndAdd = Vec3.scaleAndAdd;
|
||||
const v3cross = Vec3.cross;
|
||||
const v3slerp = Vec3.slerp;
|
||||
const v3dot = Vec3.dot;
|
||||
const v3unitX = Vec3.unitX;
|
||||
const caAdd3 = ChunkedArray.add3;
|
||||
@@ -53,7 +56,7 @@ function getCosSin(radialSegments: number, shift: boolean) {
|
||||
return CosSinCache.get(hash)!;
|
||||
}
|
||||
|
||||
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded') {
|
||||
export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, startCap: boolean, endCap: boolean, crossSection: 'elliptical' | 'rounded', roundCap = false) {
|
||||
const { currentGroup, vertices, normals, indices, groups } = state;
|
||||
|
||||
let vertexCount = vertices.elementCount;
|
||||
@@ -63,14 +66,23 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
const q1 = Math.round(radialSegments / 4);
|
||||
const q3 = q1 * 3;
|
||||
|
||||
const roundCapFlag = roundCap && linearSegments && !(startCap && endCap) && (startCap || endCap); // disabled if both caps are active
|
||||
for (let i = 0; i <= linearSegments; ++i) {
|
||||
const i3 = i * 3;
|
||||
v3fromArray(u, normalVectors, i3);
|
||||
v3fromArray(v, binormalVectors, i3);
|
||||
v3fromArray(controlPoint, controlPoints, i3);
|
||||
|
||||
const width = widthValues[i];
|
||||
const height = heightValues[i];
|
||||
let width = widthValues[i];
|
||||
let height = heightValues[i];
|
||||
let capSmoothingFactor: number;
|
||||
if (roundCapFlag) {
|
||||
capSmoothingFactor = Math.max(Number.EPSILON, Math.sqrt(1 - Math.pow((startCap ? linearSegments - i : i) / (linearSegments), 2)));
|
||||
width *= capSmoothingFactor;
|
||||
height *= capSmoothingFactor;
|
||||
v3cross(capNormalSmoothingVector, startCap ? v : u, startCap ? u : v);
|
||||
v3normalize(capNormalSmoothingVector, capNormalSmoothingVector);
|
||||
}
|
||||
const rounded = crossSection === 'rounded' && height > width;
|
||||
|
||||
for (let j = 0; j < radialSegments; ++j) {
|
||||
@@ -94,6 +106,9 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
v3normalize(normalVector, normalVector);
|
||||
|
||||
caAdd3(vertices, surfacePoint[0], surfacePoint[1], surfacePoint[2]);
|
||||
if (roundCapFlag) {
|
||||
v3slerp(normalVector, capNormalSmoothingVector, normalVector, capSmoothingFactor!);
|
||||
}
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
}
|
||||
}
|
||||
@@ -144,8 +159,8 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
const width = widthValues[0];
|
||||
let height = heightValues[0];
|
||||
const width = roundCapFlag ? 0 : widthValues[0];
|
||||
let height = roundCapFlag ? 0 : heightValues[0];
|
||||
const rounded = crossSection === 'rounded' && height > width;
|
||||
if (rounded) height -= width;
|
||||
|
||||
@@ -181,8 +196,8 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
|
||||
caAdd3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
|
||||
caAdd3(normals, normalVector[0], normalVector[1], normalVector[2]);
|
||||
|
||||
const width = widthValues[linearSegments];
|
||||
let height = heightValues[linearSegments];
|
||||
const width = roundCapFlag ? 0 : widthValues[linearSegments];
|
||||
let height = roundCapFlag ? 0 : heightValues[linearSegments];
|
||||
const rounded = crossSection === 'rounded' && height > width;
|
||||
if (rounded) height -= width;
|
||||
|
||||
|
||||
@@ -628,6 +628,7 @@ export namespace Mesh {
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
|
||||
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
@@ -713,6 +714,7 @@ export namespace Mesh {
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dCelShaded: ValueCell.create(props.celShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
@@ -734,6 +736,7 @@ export namespace Mesh {
|
||||
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
|
||||
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
|
||||
@@ -247,6 +247,7 @@ export namespace Spheres {
|
||||
sizeFactor: PD.Numeric(1, { min: 0, max: 10, step: 0.1 }),
|
||||
doubleSided: PD.Boolean(false, BaseGeometry.CustomQualityParamInfo),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
|
||||
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
|
||||
solidInterior: PD.Boolean(true, BaseGeometry.ShadingCategory),
|
||||
@@ -346,6 +347,7 @@ export namespace Spheres {
|
||||
uSizeFactor: spheres.shaderData.sizeFactor,
|
||||
uDoubleSided: ValueCell.create(props.doubleSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dCelShaded: ValueCell.create(props.celShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
dSolidInterior: ValueCell.create(props.solidInterior),
|
||||
@@ -372,6 +374,7 @@ export namespace Spheres {
|
||||
ValueCell.updateIfChanged(values.uSizeFactor, props.sizeFactor);
|
||||
ValueCell.updateIfChanged(values.uDoubleSided, props.doubleSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.dSolidInterior, props.solidInterior);
|
||||
|
||||
@@ -121,6 +121,7 @@ export namespace TextureMesh {
|
||||
flipSided: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
flatShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
ignoreLight: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
celShaded: PD.Boolean(false, BaseGeometry.ShadingCategory),
|
||||
xrayShaded: PD.Select<boolean | 'inverted'>(false, [[false, 'Off'], [true, 'On'], ['inverted', 'Inverted']], BaseGeometry.ShadingCategory),
|
||||
transparentBackfaces: PD.Select('off', PD.arrayToOptions(['off', 'on', 'opaque'] as const), BaseGeometry.ShadingCategory),
|
||||
bumpFrequency: PD.Numeric(0, { min: 0, max: 10, step: 0.1 }, BaseGeometry.ShadingCategory),
|
||||
@@ -226,6 +227,7 @@ export namespace TextureMesh {
|
||||
dFlatShaded: ValueCell.create(props.flatShaded),
|
||||
dFlipSided: ValueCell.create(props.flipSided),
|
||||
dIgnoreLight: ValueCell.create(props.ignoreLight),
|
||||
dCelShaded: ValueCell.create(props.celShaded),
|
||||
dXrayShaded: ValueCell.create(props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off'),
|
||||
dTransparentBackfaces: ValueCell.create(props.transparentBackfaces),
|
||||
uBumpFrequency: ValueCell.create(props.bumpFrequency),
|
||||
@@ -247,6 +249,7 @@ export namespace TextureMesh {
|
||||
ValueCell.updateIfChanged(values.dFlatShaded, props.flatShaded);
|
||||
ValueCell.updateIfChanged(values.dFlipSided, props.flipSided);
|
||||
ValueCell.updateIfChanged(values.dIgnoreLight, props.ignoreLight);
|
||||
ValueCell.updateIfChanged(values.dCelShaded, props.celShaded);
|
||||
ValueCell.updateIfChanged(values.dXrayShaded, props.xrayShaded === 'inverted' ? 'inverted' : props.xrayShaded === true ? 'on' : 'off');
|
||||
ValueCell.updateIfChanged(values.dTransparentBackfaces, props.transparentBackfaces);
|
||||
ValueCell.updateIfChanged(values.uBumpFrequency, props.bumpFrequency);
|
||||
|
||||
@@ -115,6 +115,30 @@ export function LocationIterator(groupCount: number, instanceCount: number, stri
|
||||
};
|
||||
}
|
||||
|
||||
export const EmptyLocationIterator: LocationIterator = {
|
||||
get hasNext() { return false; },
|
||||
get isNextNewInstance() { return false; },
|
||||
groupCount: 0,
|
||||
instanceCount: 0,
|
||||
count: 0,
|
||||
stride: 0,
|
||||
nonInstanceable: false,
|
||||
hasLocation2: false,
|
||||
move() {
|
||||
return {
|
||||
location: NullLocation as Location,
|
||||
location2: NullLocation as Location,
|
||||
index: 0,
|
||||
groupIndex: 0,
|
||||
instanceIndex: 0,
|
||||
isSecondary: false
|
||||
};
|
||||
},
|
||||
reset() {},
|
||||
skipInstance() {},
|
||||
voidInstances() {}
|
||||
};
|
||||
|
||||
//
|
||||
|
||||
/** A position Location */
|
||||
|
||||
@@ -26,6 +26,7 @@ export const CylindersSchema = {
|
||||
padding: ValueSpec('number'),
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dCelShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
|
||||
@@ -40,6 +40,7 @@ export const DirectVolumeSchema = {
|
||||
dAxisOrder: DefineSpec('string', ['012', '021', '102', '120', '201', '210']),
|
||||
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dCelShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
};
|
||||
export type DirectVolumeSchema = typeof DirectVolumeSchema
|
||||
|
||||
@@ -22,6 +22,7 @@ export const MeshSchema = {
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dCelShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
|
||||
@@ -169,6 +169,7 @@ export const GlobalUniformSchema = {
|
||||
uMarkerAverage: UniformSpec('f'),
|
||||
|
||||
uXrayEdgeFalloff: UniformSpec('f'),
|
||||
uCelSteps: UniformSpec('f'),
|
||||
uExposure: UniformSpec('f'),
|
||||
|
||||
uRenderMask: UniformSpec('i'),
|
||||
|
||||
@@ -21,6 +21,7 @@ export const SpheresSchema = {
|
||||
padding: ValueSpec('number'),
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dCelShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
dSolidInterior: DefineSpec('boolean'),
|
||||
|
||||
@@ -22,6 +22,7 @@ export const TextureMeshSchema = {
|
||||
uDoubleSided: UniformSpec('b', 'material'),
|
||||
dFlipSided: DefineSpec('boolean'),
|
||||
dIgnoreLight: DefineSpec('boolean'),
|
||||
dCelShaded: DefineSpec('boolean'),
|
||||
dXrayShaded: DefineSpec('string', ['off', 'on', 'inverted']),
|
||||
dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
|
||||
uBumpFrequency: UniformSpec('f', 'material'),
|
||||
|
||||
@@ -109,6 +109,7 @@ export const RendererParams = {
|
||||
markerPriority: PD.Select(1, [[1, 'Highlight'], [2, 'Select']]),
|
||||
|
||||
xrayEdgeFalloff: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.1 }),
|
||||
celSteps: PD.Numeric(5, { min: 2, max: 16, step: 1 }),
|
||||
exposure: PD.Numeric(1, { min: 0.0, max: 3.0, step: 0.01 }),
|
||||
|
||||
light: PD.ObjectList({
|
||||
@@ -258,6 +259,7 @@ namespace Renderer {
|
||||
uMarkerAverage: ValueCell.create(0),
|
||||
|
||||
uXrayEdgeFalloff: ValueCell.create(p.xrayEdgeFalloff),
|
||||
uCelSteps: ValueCell.create(p.celSteps),
|
||||
uExposure: ValueCell.create(p.exposure),
|
||||
};
|
||||
const globalUniformList = Object.entries(globalUniforms);
|
||||
@@ -853,6 +855,12 @@ namespace Renderer {
|
||||
p.xrayEdgeFalloff = props.xrayEdgeFalloff;
|
||||
ValueCell.update(globalUniforms.uXrayEdgeFalloff, p.xrayEdgeFalloff);
|
||||
}
|
||||
|
||||
if (props.celSteps !== undefined && props.celSteps !== p.celSteps) {
|
||||
p.celSteps = props.celSteps;
|
||||
ValueCell.update(globalUniforms.uCelSteps, p.celSteps);
|
||||
}
|
||||
|
||||
if (props.exposure !== undefined && props.exposure !== p.exposure) {
|
||||
p.exposure = props.exposure;
|
||||
ValueCell.update(globalUniforms.uExposure, p.exposure);
|
||||
|
||||
@@ -173,7 +173,7 @@ function ignoreDefine(name: string, variant: string, defines: ShaderDefines): bo
|
||||
'dLightCount', 'dXrayShaded',
|
||||
'dOverpaintType', 'dOverpaint',
|
||||
'dSubstanceType', 'dSubstance',
|
||||
'dColorMarker',
|
||||
'dColorMarker', 'dCelShaded'
|
||||
];
|
||||
if (variant !== 'emissive') {
|
||||
ignore.push('dEmissiveType', 'dEmissive');
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*
|
||||
* adapted from three.js (https://github.com/mrdoob/three.js/)
|
||||
* which under the MIT License, Copyright © 2010-2021 three.js authors
|
||||
@@ -30,7 +31,16 @@ export const apply_light_color = `
|
||||
|
||||
vec4 color = material;
|
||||
|
||||
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
|
||||
#if defined(dCelShaded)
|
||||
// clamp to avoid artifacts
|
||||
metalness = clamp(metalness, 0.0, 0.99);
|
||||
roughness = clamp(roughness, 0.05, 1.0);
|
||||
#endif
|
||||
|
||||
GeometricContext geometry;
|
||||
geometry.position = -vViewPosition;
|
||||
geometry.normal = normal;
|
||||
geometry.viewDir = normalize(vViewPosition);
|
||||
|
||||
PhysicalMaterial physicalMaterial;
|
||||
physicalMaterial.diffuseColor = color.rgb * (1.0 - metalness);
|
||||
@@ -44,30 +54,53 @@ export const apply_light_color = `
|
||||
physicalMaterial.specularColor = mix(vec3(0.04), color.rgb, metalness);
|
||||
physicalMaterial.specularF90 = 1.0;
|
||||
|
||||
GeometricContext geometry;
|
||||
geometry.position = -vViewPosition;
|
||||
geometry.normal = normal;
|
||||
geometry.viewDir = normalize(vViewPosition);
|
||||
|
||||
IncidentLight directLight;
|
||||
#pragma unroll_loop_start
|
||||
for (int i = 0; i < dLightCount; ++i) {
|
||||
directLight.direction = uLightDirection[i];
|
||||
directLight.color = uLightColor[i] * PI; // * PI for punctual light
|
||||
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
|
||||
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
|
||||
vec3 outgoingLight = vec3(0.0);
|
||||
|
||||
// indirect specular only metals
|
||||
vec3 radiance = uAmbientColor * metalness;
|
||||
vec3 iblIrradiance = uAmbientColor * metalness;
|
||||
vec3 clearcoatRadiance = vec3(0.0);
|
||||
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
|
||||
#if defined(dCelShaded)
|
||||
float celDiffuse;
|
||||
float celSpecular;
|
||||
float celIntensity;
|
||||
|
||||
vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
|
||||
#pragma unroll_loop_start
|
||||
for (int i = 0; i < dLightCount; ++i) {
|
||||
directLight.direction = uLightDirection[i];
|
||||
directLight.color = uLightColor[i] * PI; // * PI for punctual light
|
||||
|
||||
celDiffuse = RECIPROCAL_PI * max(dot(geometry.normal, directLight.direction), 0.0) * (1.0 - metalness);
|
||||
celSpecular = luminance(saturate(dot(geometry.normal, directLight.direction)) * BRDF_GGX(directLight.direction, geometry.viewDir, geometry.normal, physicalMaterial.specularColor, physicalMaterial.specularF90, roughness));
|
||||
|
||||
celIntensity = celDiffuse + celSpecular;
|
||||
celIntensity = ceil(celIntensity * uCelSteps) / uCelSteps;
|
||||
|
||||
outgoingLight += color.rgb * directLight.color * celIntensity;
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
outgoingLight += physicalMaterial.diffuseColor * luminance(uAmbientColor);
|
||||
#else
|
||||
ReflectedLight reflectedLight = ReflectedLight(vec3(0.0), vec3(0.0), vec3(0.0), vec3(0.0));
|
||||
|
||||
#pragma unroll_loop_start
|
||||
for (int i = 0; i < dLightCount; ++i) {
|
||||
directLight.direction = uLightDirection[i];
|
||||
directLight.color = uLightColor[i] * PI; // * PI for punctual light
|
||||
RE_Direct_Physical(directLight, geometry, physicalMaterial, reflectedLight);
|
||||
}
|
||||
#pragma unroll_loop_end
|
||||
|
||||
vec3 irradiance = uAmbientColor * PI; // * PI for punctual light
|
||||
RE_IndirectDiffuse_Physical(irradiance, geometry, physicalMaterial, reflectedLight);
|
||||
|
||||
// indirect specular only metals
|
||||
vec3 radiance = uAmbientColor * metalness;
|
||||
vec3 iblIrradiance = uAmbientColor * metalness;
|
||||
vec3 clearcoatRadiance = vec3(0.0);
|
||||
RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
|
||||
|
||||
outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
|
||||
#endif
|
||||
outgoingLight = clamp(outgoingLight, 0.01, 0.99); // prevents black artifacts on specular highlight with transparent background
|
||||
|
||||
#if defined(dRenderVariant_color)
|
||||
|
||||
@@ -76,6 +76,7 @@ uniform vec3 uInteriorColor;
|
||||
bool interior;
|
||||
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uCelSteps;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform mat4 uProjection;
|
||||
|
||||
@@ -103,6 +103,12 @@ vec4 linearTosRGB(const in vec4 c) {
|
||||
return vec4(mix(pow(c.rgb, vec3(0.41666)) * 1.055 - vec3(0.055), c.rgb * 12.92, vec3(lessThanEqual(c.rgb, vec3(0.0031308)))), c.a);
|
||||
}
|
||||
|
||||
float luminance(vec3 c) {
|
||||
// https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
|
||||
const vec3 W = vec3(0.2125, 0.7154, 0.0721);
|
||||
return dot(c, W);
|
||||
}
|
||||
|
||||
float linearizeDepth(const in float depth, const in float near, const in float far) {
|
||||
return (2.0 * near) / (far + near - depth * (far - near));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) 2020-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Gianluca Tomasello <giagitom@gmail.com>
|
||||
*/
|
||||
|
||||
export const cylinders_frag = `
|
||||
@@ -120,7 +121,11 @@ bool CylinderImpostor(
|
||||
#ifdef dSolidInterior
|
||||
if (interior) cameraNormal = -rayDir;
|
||||
#endif
|
||||
return true;
|
||||
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
|
||||
return true;
|
||||
#else
|
||||
return !interior;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
} else if (bottomCap && y >= 0.0) {
|
||||
@@ -146,7 +151,11 @@ bool CylinderImpostor(
|
||||
#ifdef dSolidInterior
|
||||
if (interior) cameraNormal = -rayDir;
|
||||
#endif
|
||||
return true;
|
||||
#if defined(dClipVariant_pixel) && dClipObjectCount != 0
|
||||
return true;
|
||||
#else
|
||||
return !interior;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ uniform vec3 uFogColor;
|
||||
uniform float uAlpha;
|
||||
uniform bool uTransparentBackground;
|
||||
uniform float uXrayEdgeFalloff;
|
||||
uniform float uCelSteps;
|
||||
uniform float uExposure;
|
||||
|
||||
uniform int uRenderMask;
|
||||
|
||||
120
src/mol-gl/shader/dof.frag.ts
Normal file
120
src/mol-gl/shader/dof.frag.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Ludovic Autin <autin@scripps.edu>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
export const dof_frag = `
|
||||
precision highp float;
|
||||
precision highp int;
|
||||
precision highp sampler2D;
|
||||
|
||||
#include common
|
||||
|
||||
uniform sampler2D tColor;
|
||||
uniform sampler2D tDepthOpaque;
|
||||
uniform sampler2D tDepthTransparent;
|
||||
|
||||
uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
uniform float uBlurSpread;
|
||||
uniform float uInFocus;
|
||||
uniform float uPPM;
|
||||
|
||||
uniform float uNear; // Near plane
|
||||
uniform float uFar; // Far plane
|
||||
|
||||
uniform mat4 uInvProjection; // Inverse projection
|
||||
uniform mat4 uProjection; // projection
|
||||
|
||||
uniform int uMode; // 0-planar, 1-spherical
|
||||
uniform vec3 uCenter; // Center of focus sphere in view space
|
||||
|
||||
// Function to convert depth value from depth buffer to view space Z
|
||||
float getViewZ(const in float depth) {
|
||||
#if dOrthographic == 1
|
||||
return orthographicDepthToViewZ(depth, uNear, uFar);
|
||||
#else
|
||||
return perspectiveDepthToViewZ(depth, uNear, uFar);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Retrieve depth from opaque depth texture
|
||||
float getDepthOpaque(const in vec2 coords) {
|
||||
#ifdef depthTextureSupport
|
||||
return texture2D(tDepthOpaque, coords).r;
|
||||
#else
|
||||
return unpackRGBAToDepth(texture2D(tDepthOpaque, coords));
|
||||
#endif
|
||||
}
|
||||
|
||||
// Retrieve depth from transparent depth texture
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
float getDepth(const in vec2 coords) {
|
||||
return min(getDepthOpaque(coords), getDepthTransparent(coords));
|
||||
}
|
||||
|
||||
float getCOC(vec2 uv) {
|
||||
float depth = getDepth(uv);
|
||||
float viewDist = getViewZ(depth);
|
||||
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
|
||||
float focusDist = length(aposition - uCenter);
|
||||
float coc = 0.0; // Circle of Confusion
|
||||
if (uMode == 0) { // planar Depth of field
|
||||
coc = (abs(viewDist) - uInFocus) / uPPM; //focus distance, focus range
|
||||
} else if(uMode == 1) { // spherical Depth of field
|
||||
coc = focusDist / uPPM ;
|
||||
}
|
||||
coc = clamp(coc, -1.0, 1.0);
|
||||
return coc;
|
||||
}
|
||||
|
||||
// Simple box blur for blurring the image
|
||||
vec3 getBlurredImage(vec2 coords) {
|
||||
vec4 blurColor = vec4(0);
|
||||
vec2 texelSize = vec2(1.0 / uTexSize.x, 1.0 / uTexSize.y);
|
||||
float count = 0.0;
|
||||
for (int x = 0; x < int(dBlurSize); x++) {
|
||||
for (int y = 0; y < int(dBlurSize); y++) {
|
||||
vec2 offset = vec2(float(x) - float(dBlurSize) / 2.0, float(y) - float(dBlurSize) / 2.0);
|
||||
vec2 uvPixel = coords.xy + offset * texelSize * uBlurSpread;
|
||||
float coc = getCOC(uvPixel);
|
||||
coc = smoothstep(0.0, 1.0, abs(coc));
|
||||
// mix blurColor with new color with weight coc
|
||||
blurColor.rgb = blurColor.rgb + texture2D(tColor, uvPixel).xyz * coc;
|
||||
count+=coc;
|
||||
}
|
||||
}
|
||||
blurColor = blurColor / count;
|
||||
return blurColor.rgb;
|
||||
}
|
||||
|
||||
// simplification from https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/
|
||||
void main() {
|
||||
vec2 uv = gl_FragCoord.xy / uTexSize;
|
||||
vec4 color = texture2D(tColor, uv);
|
||||
float depth = getDepth(uv);
|
||||
|
||||
float viewDist = getViewZ(depth);
|
||||
|
||||
vec3 aposition = screenSpaceToViewSpace(vec3(uv.xy, depth), uInvProjection);
|
||||
float focusDist = length(aposition - uCenter);
|
||||
vec3 blurColor = getBlurredImage(uv);
|
||||
|
||||
float coc = getCOC(uv); // Circle of Confusion
|
||||
|
||||
// for debugging the coc
|
||||
// color.rgb = (coc < 0.0) ? (1.0 - abs(coc)) * vec3(1.0,0.0,0.0) : vec3(0.0, 1.0 - coc, 0.0) ;//mix(color.rgb, blurColor.rgb, abs(coc));
|
||||
color.rgb = mix(color.rgb, blurColor, smoothstep(0.0, 1.0, abs(coc))); // Smooth blending based on CoC
|
||||
gl_FragColor = color;
|
||||
}
|
||||
`;
|
||||
@@ -13,7 +13,6 @@ precision highp sampler2D;
|
||||
uniform sampler2D tSsaoDepth;
|
||||
uniform sampler2D tColor;
|
||||
uniform sampler2D tDepthOpaque;
|
||||
uniform sampler2D tDepthTransparent;
|
||||
uniform sampler2D tShadows;
|
||||
uniform sampler2D tOutlines;
|
||||
uniform vec2 uTexSize;
|
||||
@@ -46,14 +45,6 @@ float getDepthOpaque(const in vec2 coords) {
|
||||
#endif
|
||||
}
|
||||
|
||||
float getDepthTransparent(const in vec2 coords) {
|
||||
#ifdef dTransparentOutline
|
||||
return unpackRGBAToDepth(texture2D(tDepthTransparent, coords));
|
||||
#else
|
||||
return 1.0;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
@@ -62,11 +53,6 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
|
||||
float backgroundViewZ = 2.0 * uFar;
|
||||
vec2 invTexSize = 1.0 / uTexSize;
|
||||
|
||||
float transparentDepth = getDepthTransparent(coords);
|
||||
float opaqueSelfViewZ = isBackground(opaqueDepth) ? backgroundViewZ : getViewZ(opaqueDepth);
|
||||
float transparentSelfViewZ = isBackground(transparentDepth) ? backgroundViewZ : getViewZ(transparentDepth);
|
||||
float selfDepth = min(opaqueDepth, transparentDepth);
|
||||
|
||||
float outline = 1.0;
|
||||
closestTexel = 1.0;
|
||||
for (int y = -dOutlineScale; y <= dOutlineScale; y++) {
|
||||
@@ -80,9 +66,7 @@ float getOutline(const in vec2 coords, const in float opaqueDepth, out float clo
|
||||
vec4 sampleOutlineCombined = texture2D(tOutlines, sampleCoords);
|
||||
float sampleOutline = sampleOutlineCombined.r;
|
||||
float sampleOutlineDepth = unpackRGToUnitInterval(sampleOutlineCombined.gb);
|
||||
float sampleOutlineViewZ = isBackground(sampleOutlineDepth) ? backgroundViewZ : getViewZ(sampleOutlineDepth);
|
||||
|
||||
float selfViewZ = sampleOutlineCombined.a == 0.0 ? opaqueSelfViewZ : transparentSelfViewZ;
|
||||
if (sampleOutline == 0.0 && sampleOutlineDepth < closestTexel) {
|
||||
outline = 0.0;
|
||||
closestTexel = sampleOutlineDepth;
|
||||
|
||||
@@ -16,6 +16,7 @@ uniform vec2 uTexSize;
|
||||
uniform vec4 uBounds;
|
||||
|
||||
uniform float uKernel[dOcclusionKernelSize];
|
||||
uniform float uBlurDepthBias;
|
||||
|
||||
uniform float uBlurDirectionX;
|
||||
uniform float uBlurDirectionY;
|
||||
@@ -38,6 +39,10 @@ bool isBackground(const in float depth) {
|
||||
return depth == 1.0;
|
||||
}
|
||||
|
||||
bool isNearClip(const in float depth) {
|
||||
return depth == 0.0;
|
||||
}
|
||||
|
||||
bool outsideBounds(const in vec2 p) {
|
||||
return p.x < uBounds.x || p.y < uBounds.y || p.x > uBounds.z || p.y > uBounds.w;
|
||||
}
|
||||
@@ -59,16 +64,14 @@ void main(void) {
|
||||
}
|
||||
|
||||
float selfDepth = unpackRGToUnitInterval(packedDepth);
|
||||
// if background and if second pass
|
||||
if (isBackground(selfDepth) && uBlurDirectionY != 0.0) {
|
||||
// (if background and if second pass) or if near clip
|
||||
if ((isBackground(selfDepth) && uBlurDirectionY != 0.0) || isNearClip(selfDepth)) {
|
||||
gl_FragColor = vec4(packUnitIntervalToRG(1.0), packedDepth);
|
||||
return;
|
||||
}
|
||||
|
||||
float selfViewZ = getViewZ(selfDepth);
|
||||
float pixelSize = getPixelSize(coords, selfDepth);
|
||||
// max diff depth between two pixels
|
||||
float maxDiffViewZ = 1.0;
|
||||
|
||||
vec2 offset = vec2(uBlurDirectionX, uBlurDirectionY) / uTexSize;
|
||||
|
||||
@@ -86,12 +89,12 @@ void main(void) {
|
||||
vec4 sampleSsaoDepth = texture2D(tSsaoDepth, sampleCoords);
|
||||
|
||||
float sampleDepth = unpackRGToUnitInterval(sampleSsaoDepth.zw);
|
||||
if (isBackground(sampleDepth)) {
|
||||
if (isBackground(sampleDepth) || isNearClip(sampleDepth)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
float sampleViewZ = getViewZ(sampleDepth);
|
||||
if (abs(selfViewZ - sampleViewZ) > maxDiffViewZ) {
|
||||
if (abs(selfViewZ - sampleViewZ) >= uBlurDepthBias) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -344,6 +344,7 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
|
||||
contextRestored,
|
||||
setContextLost: () => {
|
||||
isContextLost = true;
|
||||
timer.clear();
|
||||
},
|
||||
handleContextRestored: (extraResets?: () => void) => {
|
||||
Object.assign(extensions, createExtensions(gl));
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2022-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
@@ -33,6 +33,10 @@ class MovingAverage {
|
||||
return Object.fromEntries(this.avgs.entries());
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.avgs.clear();
|
||||
}
|
||||
|
||||
constructor(private count: number) { }
|
||||
}
|
||||
|
||||
@@ -111,8 +115,13 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions,
|
||||
dtq.deleteQuery(query);
|
||||
});
|
||||
pending.clear();
|
||||
stack.length = 0;
|
||||
gpuAvgs.clear();
|
||||
cpuAvgs.clear();
|
||||
|
||||
measures = [];
|
||||
current = null;
|
||||
capturingStats = false;
|
||||
};
|
||||
|
||||
const add = () => {
|
||||
@@ -132,7 +141,8 @@ export function createTimer(gl: GLRenderingContext, extensions: WebGLExtensions,
|
||||
return {
|
||||
resolve: () => {
|
||||
const results: TimerResult[] = [];
|
||||
if (!dtq || !measures.length) return results;
|
||||
if (!dtq || !measures.length || capturingStats) return results;
|
||||
|
||||
// console.log('resolve');
|
||||
queries.forEach((result, query) => {
|
||||
if (result.timeElapsed !== undefined) return;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
|
||||
* Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.396, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
|
||||
* Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.396, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.391, IHM 1.25, MA 1.4.6.
|
||||
* Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.396, IHM 1.26, MA 1.4.6.
|
||||
*
|
||||
* @author molstar/ciftools package
|
||||
*/
|
||||
@@ -290,7 +290,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* Pointer to _atom_site.auth_seq_id
|
||||
*/
|
||||
pdbx_auth_seq_id: str,
|
||||
pdbx_auth_seq_id: int,
|
||||
/**
|
||||
* Pointer to _atom_site.auth_asym_id
|
||||
*/
|
||||
@@ -793,6 +793,14 @@ export const mmCIF_Schema = {
|
||||
* the entry.
|
||||
*/
|
||||
pdbx_number_of_molecules: int,
|
||||
/**
|
||||
* An identifier for the parent entity if this entity
|
||||
* is part of a complex entity. For instance a chimeric
|
||||
* entity may be decomposed into several independent
|
||||
* chemical entities where each component entity was
|
||||
* obtained from a different source.
|
||||
*/
|
||||
pdbx_parent_entity_id: str,
|
||||
/**
|
||||
* Details about any entity mutation(s).
|
||||
*/
|
||||
@@ -1025,6 +1033,10 @@ export const mmCIF_Schema = {
|
||||
* and to distinguish this structural result from others.
|
||||
*/
|
||||
title: str,
|
||||
/**
|
||||
* Indicates if the structure was determined using experimental, computational, or integrative methods
|
||||
*/
|
||||
pdbx_structure_determination_methodology: Aliased<'experimental' | 'integrative' | 'computational'>(str),
|
||||
/**
|
||||
* An automatically generated descriptor for an NDB structure or
|
||||
* the unstructured content of the PDB COMPND record.
|
||||
@@ -1483,6 +1495,116 @@ export const mmCIF_Schema = {
|
||||
*/
|
||||
pdbx_keywords: str,
|
||||
},
|
||||
/**
|
||||
* Data items in the STRUCT_MON_PROT_CIS category identify
|
||||
* monomers that have been found to have the peptide bond in the cis
|
||||
* conformation. The criterion used to select residues to be
|
||||
* designated as containing cis peptide bonds is given in
|
||||
* _struct_mon_details.prot_cis.
|
||||
*/
|
||||
struct_mon_prot_cis: {
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_sites_alt.id in the
|
||||
* ATOM_SITES_ALT category.
|
||||
*/
|
||||
label_alt_id: str,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.label_asym_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
label_asym_id: str,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.label_comp_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
label_comp_id: str,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.label_seq_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
label_seq_id: int,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.auth_asym_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_asym_id: str,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.auth_comp_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_comp_id: str,
|
||||
/**
|
||||
* A component of the identifier for the monomer.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.auth_seq_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_seq_id: int,
|
||||
/**
|
||||
* Pointer to _atom_site.auth_asym_id.
|
||||
*/
|
||||
pdbx_auth_asym_id_2: str,
|
||||
/**
|
||||
* Pointer to _atom_site.auth_comp_id.
|
||||
*/
|
||||
pdbx_auth_comp_id_2: str,
|
||||
/**
|
||||
* Pointer to _atom_site.auth_seq_id
|
||||
*/
|
||||
pdbx_auth_seq_id_2: int,
|
||||
/**
|
||||
* Pointer to _atom_site.label_asym_id.
|
||||
*/
|
||||
pdbx_label_asym_id_2: str,
|
||||
/**
|
||||
* Pointer to _atom_site.label_comp_id.
|
||||
*/
|
||||
pdbx_label_comp_id_2: str,
|
||||
/**
|
||||
* Pointer to _atom_site.label_seq_id
|
||||
*/
|
||||
pdbx_label_seq_id_2: int,
|
||||
/**
|
||||
* Pointer to _atom_site.pdbx_PDB_ins_code
|
||||
*/
|
||||
pdbx_PDB_ins_code: str,
|
||||
/**
|
||||
* Pointer to _atom_site.pdbx_PDB_ins_code
|
||||
*/
|
||||
pdbx_PDB_ins_code_2: str,
|
||||
/**
|
||||
* Pointer to _atom_site.pdbx_PDB_model_num
|
||||
*/
|
||||
pdbx_PDB_model_num: int,
|
||||
/**
|
||||
* omega torsion angle
|
||||
*/
|
||||
pdbx_omega_angle: str,
|
||||
/**
|
||||
* ordinal index
|
||||
*/
|
||||
pdbx_id: str,
|
||||
/**
|
||||
* PDB Insertion code
|
||||
*/
|
||||
pdbx_auth_ins_code: str,
|
||||
/**
|
||||
* PDB Insertion code
|
||||
*/
|
||||
pdbx_auth_ins_code_2: str,
|
||||
},
|
||||
/**
|
||||
* Data items in the STRUCT_NCS_OPER category describe the
|
||||
* noncrystallographic symmetry operations.
|
||||
@@ -1699,7 +1821,7 @@ export const mmCIF_Schema = {
|
||||
* This data item is a pointer to _atom_site.auth_seq_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
pdbx_auth_seq_id: str,
|
||||
pdbx_auth_seq_id: int,
|
||||
/**
|
||||
* PDB insertion code for the ligand in the site.
|
||||
*/
|
||||
@@ -1766,6 +1888,13 @@ export const mmCIF_Schema = {
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_asym_id: str,
|
||||
/**
|
||||
* A component of the identifier for participants in the site.
|
||||
*
|
||||
* This data item is a pointer to _atom_site.auth_atom_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_atom_id: str,
|
||||
/**
|
||||
* A component of the identifier for participants in the site.
|
||||
*
|
||||
@@ -1779,7 +1908,7 @@ export const mmCIF_Schema = {
|
||||
* This data item is a pointer to _atom_site.auth_seq_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_seq_id: str,
|
||||
auth_seq_id: int,
|
||||
/**
|
||||
* This data item is a pointer to _struct_site.id in the STRUCT_SITE
|
||||
* category.
|
||||
@@ -1800,6 +1929,21 @@ export const mmCIF_Schema = {
|
||||
*/
|
||||
pdbx_num_res: int,
|
||||
},
|
||||
/**
|
||||
* Data items in the STRUCT_SITE_KEYWORDS category record
|
||||
* keywords describing the site.
|
||||
*/
|
||||
struct_site_keywords: {
|
||||
/**
|
||||
* This data item is a pointer to _struct_site.id in the STRUCT_SITE
|
||||
* category.
|
||||
*/
|
||||
site_id: str,
|
||||
/**
|
||||
* Keywords describing this site.
|
||||
*/
|
||||
text: str,
|
||||
},
|
||||
/**
|
||||
* Data items in the SYMMETRY category record details about the
|
||||
* space-group symmetry.
|
||||
@@ -2082,7 +2226,7 @@ export const mmCIF_Schema = {
|
||||
* This data item is a pointer to _atom_site.auth_seq_id in the
|
||||
* ATOM_SITE category.
|
||||
*/
|
||||
auth_seq_id: str,
|
||||
auth_seq_id: int,
|
||||
/**
|
||||
* Part of the identifier for the unobserved or zero occupancy residue.
|
||||
*
|
||||
@@ -3508,7 +3652,7 @@ export const mmCIF_Schema = {
|
||||
},
|
||||
/**
|
||||
* Data items in the IHM_MODEL_LIST category record the
|
||||
* details of the models being deposited.
|
||||
* details of the structure models being deposited.
|
||||
*/
|
||||
ihm_model_list: {
|
||||
/**
|
||||
@@ -3540,7 +3684,7 @@ export const mmCIF_Schema = {
|
||||
},
|
||||
/**
|
||||
* IHM_MODEL_GROUP category defines collections or groups of integrative
|
||||
* structural models.
|
||||
* structure models.
|
||||
*/
|
||||
ihm_model_group: {
|
||||
/**
|
||||
@@ -3568,8 +3712,8 @@ export const mmCIF_Schema = {
|
||||
details: str,
|
||||
},
|
||||
/**
|
||||
* IHM_MODEL_GROUP_LINK category provides the list of models present in
|
||||
* a particular model group.
|
||||
* IHM_MODEL_GROUP_LINK category provides the list of structure models present in
|
||||
* a particular structure model group.
|
||||
*/
|
||||
ihm_model_group_link: {
|
||||
/**
|
||||
@@ -3587,7 +3731,7 @@ export const mmCIF_Schema = {
|
||||
},
|
||||
/**
|
||||
* Data items in the IHM_MODEL_REPRESENTATIVE category record the
|
||||
* details of the representative model in an ensemble or cluster.
|
||||
* details of the representative structure model in an ensemble or cluster.
|
||||
*/
|
||||
ihm_model_representative: {
|
||||
/**
|
||||
@@ -3628,7 +3772,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* The type of data held in the dataset.
|
||||
*/
|
||||
data_type: Aliased<'NMR data' | '3DEM volume' | '2DEM class average' | 'EM raw micrographs' | 'X-ray diffraction data' | 'SAS data' | 'CX-MS data' | 'Mass Spectrometry data' | 'EPR data' | 'H/D exchange data' | 'Single molecule FRET data' | 'Ensemble FRET data' | 'Experimental model' | 'Comparative model' | 'Integrative model' | 'De Novo model' | 'Predicted contacts' | 'Mutagenesis data' | 'DNA footprinting data' | 'Hydroxyl radical footprinting data' | 'Yeast two-hybrid screening data' | 'Quantitative measurements of genetic interactions' | 'Other'>(str),
|
||||
data_type: Aliased<'NMR data' | '3DEM volume' | '2DEM class average' | 'EM raw micrographs' | 'X-ray diffraction data' | 'SAS data' | 'CX-MS data' | 'Crosslinking-MS data' | 'Mass Spectrometry data' | 'EPR data' | 'H/D exchange data' | 'Single molecule FRET data' | 'Ensemble FRET data' | 'Experimental model' | 'Comparative model' | 'Integrative model' | 'De Novo model' | 'Predicted contacts' | 'Mutagenesis data' | 'DNA footprinting data' | 'Hydroxyl radical footprinting data' | 'Yeast two-hybrid screening data' | 'Quantitative measurements of genetic interactions' | 'Other'>(str),
|
||||
/**
|
||||
* A flag that indicates whether the dataset is archived in
|
||||
* an IHM related database or elsewhere.
|
||||
@@ -3650,7 +3794,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* The application / utilization of the dataset group in modeling.
|
||||
*/
|
||||
application: Aliased<'restraint' | 'validation' | 'filter' | 'representation' | 'sampling' | 'other'>(str),
|
||||
application: Aliased<'restraint' | 'validation' | 'filter' | 'representation' | 'sampling' | 'modeling' | 'other'>(str),
|
||||
/**
|
||||
* Additional details regarding the dataset group.
|
||||
*/
|
||||
@@ -3710,7 +3854,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* The name of the database containing the dataset entry.
|
||||
*/
|
||||
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'Other'>(str),
|
||||
db_name: Aliased<'PDB' | 'PDB-Dev' | 'BMRB' | 'EMDB' | 'EMPIAR' | 'SASBDB' | 'PRIDE' | 'MODEL ARCHIVE' | 'MASSIVE' | 'BioGRID' | 'ProXL' | 'jPOSTrepo' | 'iProX' | 'AlphaFoldDB' | 'ProteomeXchange' | 'Other'>(str),
|
||||
/**
|
||||
* The accession code for the database entry.
|
||||
*/
|
||||
@@ -4037,7 +4181,7 @@ export const mmCIF_Schema = {
|
||||
/**
|
||||
* The type of crosslinker used.
|
||||
*/
|
||||
linker_type: Aliased<'EDC' | 'DSS' | 'EGS' | 'BS3' | 'BS2G' | 'DST' | 'sulfo-SDA' | 'sulfo-SMCC' | 'DSSO' | 'DSG' | 'BSP' | 'BMSO' | 'DHSO' | 'CYS' | 'SDA' | 'DSA' | 'BrdU' | 'LCSDA' | 'CDI' | 'ADH' | 'L-Photo-Leucine' | 'KArGO' | 'BrEtY' | 'DSBU' | 'DSPP' | 'TBDSPP' | 'Other'>(str),
|
||||
linker_type: Aliased<'EDC' | 'DSS' | 'EGS' | 'BS3' | 'BS2G' | 'DST' | 'sulfo-SDA' | 'sulfo-SMCC' | 'DSSO' | 'DSG' | 'BSP' | 'BMSO' | 'DHSO' | 'CYS' | 'SDA' | 'DSA' | 'BrdU' | 'LCSDA' | 'CDI' | 'ADH' | 'L-Photo-Leucine' | 'KArGO' | 'BrEtY' | 'DSBU' | 'DSPP' | 'TBDSPP' | 'DMTMM' | 'PDH' | 'Other'>(str),
|
||||
/**
|
||||
* Identifier to the crosslinking dataset.
|
||||
* This data item is a pointer to the _ihm_dataset_list.id in the
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2022 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>
|
||||
*/
|
||||
@@ -126,10 +126,13 @@ async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<
|
||||
result.version = line.substring(8).trim();
|
||||
} else if (line.startsWith('%FLAG')) {
|
||||
const flag = line.substring(5).trim();
|
||||
const formatLine = readLine(state.tokenizer).trim();
|
||||
if (!formatLine.startsWith('%FORMAT')) throw new Error('expected %FORMAT');
|
||||
let formatLine = readLine(state.tokenizer).trim();
|
||||
while (formatLine.startsWith('%COMMENT')) {
|
||||
formatLine = readLine(state.tokenizer).trim();
|
||||
}
|
||||
if (!formatLine.startsWith('%FORMAT')) throw new Error(`expected %FORMAT got "${formatLine}"`);
|
||||
|
||||
if (flag === 'TITLE') {
|
||||
if (flag === 'TITLE' || flag === 'CTITLE') {
|
||||
result.title = handleTitle(state);
|
||||
} else if (flag === 'POINTERS') {
|
||||
result.pointers = handlePointers(state);
|
||||
|
||||
@@ -80,7 +80,8 @@ function computeN(state: RmsdTransformState) {
|
||||
|
||||
let sizeSq = 0.0;
|
||||
|
||||
for (let i = 0, _l = state.a.x.length; i < _l; i++) {
|
||||
const L = Math.min(state.a.x.length, state.b.x.length);
|
||||
for (let i = 0; i < L; i++) {
|
||||
const aX = xsA[i] - cA[0], aY = ysA[i] - cA[1], aZ = zsA[i] - cA[2];
|
||||
const bX = xsB[i] - cB[0], bY = ysB[i] - cB[1], bZ = zsB[i] - cB[2];
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2017-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Fred Ludlow <Fred.Ludlow@astx.com>
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { degToRad } from '../../../mol-math/misc';
|
||||
@@ -79,8 +80,9 @@ const tmpPosX = Vec3();
|
||||
/**
|
||||
* Calculate the angles x-a1-a2 for all x where x is a heavy atom (not H) bonded to ap1.
|
||||
*/
|
||||
export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex): number[] {
|
||||
export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex, ignoreHydrogens = true): [number[], number[]] {
|
||||
const angles: number[] = [];
|
||||
const anglesH: number[] = [];
|
||||
unitA.conformation.position(unitA.elements[indexA], tmpPosA);
|
||||
unitB.conformation.position(unitB.elements[indexB], tmpPosB);
|
||||
Vec3.sub(tmpDir1, tmpPosB, tmpPosA);
|
||||
@@ -90,9 +92,13 @@ export function calcAngles(structure: Structure, unitA: Unit.Atomic, indexA: Str
|
||||
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
|
||||
Vec3.sub(tmpDir2, tmpPosX, tmpPosA);
|
||||
angles.push(Vec3.angle(tmpDir1, tmpDir2));
|
||||
} else if (!ignoreHydrogens) {
|
||||
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
|
||||
Vec3.sub(tmpDir2, tmpPosX, tmpPosA);
|
||||
anglesH.push(Vec3.angle(tmpDir1, tmpDir2));
|
||||
}
|
||||
});
|
||||
return angles;
|
||||
return [angles, anglesH];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,4 +143,23 @@ export function calcPlaneAngle(structure: Structure, unitA: Unit.Atomic, indexA:
|
||||
|
||||
Vec3.cross(tmpDir2, neighbours[0], neighbours[1]);
|
||||
return Math.abs((Math.PI / 2) - Vec3.angle(tmpDir2, tmpDir1));
|
||||
}
|
||||
|
||||
export function closestHydrogenIndex(structure: Structure, unitA: Unit.Atomic, indexA: StructureElement.UnitIndex, unitB: Unit.Atomic, indexB: StructureElement.UnitIndex) {
|
||||
let hIndex = indexA;
|
||||
unitA.conformation.position(unitA.elements[indexA], tmpPosA);
|
||||
unitB.conformation.position(unitB.elements[indexB], tmpPosB);
|
||||
Vec3.sub(tmpDir1, tmpPosB, tmpPosA);
|
||||
let minDistSq = Vec3.squaredDistance(tmpPosA, tmpPosB);
|
||||
eachBondedAtom(structure, unitA, indexA, (unitX: Unit.Atomic, indexX: StructureElement.UnitIndex) => {
|
||||
if (typeSymbol(unitX, indexX) === Elements.H) {
|
||||
unitX.conformation.position(unitX.elements[indexX], tmpPosX);
|
||||
const dist = Vec3.squaredDistance(tmpPosX, tmpPosB);
|
||||
if (dist < minDistSq) {
|
||||
minDistSq = dist;
|
||||
hIndex = indexX;
|
||||
}
|
||||
}
|
||||
});
|
||||
return hIndex;
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Fred Ludlow <Fred.Ludlow@astx.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*
|
||||
* based in part on NGL (https://github.com/arose/ngl)
|
||||
*/
|
||||
@@ -97,12 +98,12 @@ function testHalogenBond(structure: Structure, infoA: Features.Info, infoB: Feat
|
||||
const donIndex = don.members[don.offsets[don.feature]];
|
||||
const accIndex = acc.members[acc.offsets[acc.feature]];
|
||||
|
||||
const halogenAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
|
||||
const [halogenAngles] = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
|
||||
// Singly bonded halogen only (not bromide ion for example)
|
||||
if (halogenAngles.length !== 1) return;
|
||||
if (OptimalHalogenAngle - halogenAngles[0] > opts.angleMax) return;
|
||||
|
||||
const acceptorAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
|
||||
const [acceptorAngles] = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
|
||||
// Angle must be defined. Excludes water as acceptor. Debatable
|
||||
if (acceptorAngles.length === 0) return;
|
||||
if (acceptorAngles.some(acceptorAngle => OptimalAcceptorAngle - acceptorAngle > opts.angleMax)) return;
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Fred Ludlow <Fred.Ludlow@astx.com>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*
|
||||
* based in part on NGL (https://github.com/arose/ngl)
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
import { Structure, Unit, StructureElement } from '../../../mol-model/structure';
|
||||
import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle } from '../chemistry/geometry';
|
||||
import { AtomGeometry, AtomGeometryAngles, calcAngles, calcPlaneAngle, closestHydrogenIndex } from '../chemistry/geometry';
|
||||
import { FeaturesBuilder, Features } from './features';
|
||||
import { typeSymbol, bondToElementCount, bondCount, formalCharge, compId, atomId } from '../chemistry/util';
|
||||
import { Elements } from '../../../mol-model/structure/model/properties/atomic/types';
|
||||
@@ -23,6 +24,7 @@ const GeometryParams = {
|
||||
distanceMax: PD.Numeric(3.5, { min: 1, max: 5, step: 0.1 }),
|
||||
backbone: PD.Boolean(true, { description: 'Include backbone-to-backbone hydrogen bonds' }),
|
||||
accAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal acceptor angle' }),
|
||||
ignoreHydrogens: PD.Boolean(false, { description: 'Ignore explicit hydrogens in geometric constraints' }),
|
||||
donAngleDevMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }, { description: 'Max deviation from ideal donor angle' }),
|
||||
accOutOfPlaneAngleMax: PD.Numeric(90, { min: 0, max: 180, step: 1 }),
|
||||
donOutOfPlaneAngleMax: PD.Numeric(45, { min: 0, max: 180, step: 1 }),
|
||||
@@ -208,6 +210,7 @@ function isWeakHydrogenBond(ti: FeatureType, tj: FeatureType) {
|
||||
|
||||
function getGeometryOptions(props: GeometryProps) {
|
||||
return {
|
||||
ignoreHydrogens: props.ignoreHydrogens,
|
||||
includeBackbone: props.backbone,
|
||||
maxAccAngleDev: degToRad(props.accAngleDevMax),
|
||||
maxDonAngleDev: degToRad(props.donAngleDevMax),
|
||||
@@ -230,26 +233,32 @@ type HydrogenBondsOptions = ReturnType<typeof getHydrogenBondsOptions>
|
||||
const deg120InRad = degToRad(120);
|
||||
|
||||
function checkGeometry(structure: Structure, don: Features.Info, acc: Features.Info, opts: GeometryOptions): true | undefined {
|
||||
|
||||
const donIndex = don.members[don.offsets[don.feature]];
|
||||
const accIndex = acc.members[acc.offsets[acc.feature]];
|
||||
|
||||
if (!opts.includeBackbone && isBackboneHydrogenBond(don.unit, donIndex, acc.unit, accIndex)) return;
|
||||
|
||||
const donAngles = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex);
|
||||
const [donAngles, donHAngles] = calcAngles(structure, don.unit, donIndex, acc.unit, accIndex, opts.ignoreHydrogens);
|
||||
const idealDonAngle = AtomGeometryAngles.get(don.idealGeometry[donIndex]) || deg120InRad;
|
||||
if (donAngles.some(donAngle => Math.abs(idealDonAngle - donAngle) > opts.maxDonAngleDev)) return;
|
||||
if (donHAngles.length && !donHAngles.some(donHAngles => donHAngles < opts.maxDonAngleDev)) return;
|
||||
|
||||
if (don.idealGeometry[donIndex] === AtomGeometry.Trigonal) {
|
||||
const outOfPlane = calcPlaneAngle(structure, don.unit, donIndex, acc.unit, accIndex);
|
||||
if (outOfPlane !== undefined && outOfPlane > opts.maxDonOutOfPlaneAngle) return;
|
||||
}
|
||||
|
||||
const accAngles = calcAngles(structure, acc.unit, accIndex, don.unit, donIndex);
|
||||
let donorIndex = donIndex;
|
||||
if (!opts.ignoreHydrogens && donHAngles.length > 0) {
|
||||
donorIndex = closestHydrogenIndex(structure, don.unit, donIndex, acc.unit, accIndex);
|
||||
}
|
||||
|
||||
const [accAngles, accHAngles] = calcAngles(structure, acc.unit, accIndex, don.unit, donorIndex, opts.ignoreHydrogens);
|
||||
const idealAccAngle = AtomGeometryAngles.get(acc.idealGeometry[accIndex]) || deg120InRad;
|
||||
|
||||
// Do not limit large acceptor angles
|
||||
if (accAngles.some(accAngle => idealAccAngle - accAngle > opts.maxAccAngleDev)) return;
|
||||
if (accHAngles.some(accHAngles => idealAccAngle - accHAngles > opts.maxAccAngleDev)) return;
|
||||
|
||||
if (acc.idealGeometry[accIndex] === AtomGeometry.Trigonal) {
|
||||
const outOfPlane = calcPlaneAngle(structure, acc.unit, accIndex, don.unit, donIndex);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { ParamDefinition as PD } from '../../../mol-util/param-definition';
|
||||
@@ -19,7 +20,7 @@ import { Interval, OrderedSet, SortedArray } from '../../../mol-data/int';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { InteractionsProvider } from '../interactions';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { InteractionFlag, InteractionType } from '../interactions/common';
|
||||
import { FeatureType, InteractionFlag, InteractionType } from '../interactions/common';
|
||||
import { Unit } from '../../../mol-model/structure/structure';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
@@ -54,7 +55,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
|
||||
const uB = structure.unitMap.get(unitB) as Unit.Atomic;
|
||||
|
||||
if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && (
|
||||
t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond)
|
||||
t === InteractionType.HydrogenBond || (t === InteractionType.WeakHydrogenBond && ignoreHydrogensVariant !== 'non-polar'))
|
||||
) {
|
||||
const idxA = fA.members[fA.offsets[indexA]];
|
||||
const idxB = fB.members[fB.offsets[indexB]];
|
||||
@@ -64,30 +65,34 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
|
||||
let minDistB = minDistA;
|
||||
Vec3.copy(posA, pA);
|
||||
Vec3.copy(posB, pB);
|
||||
const donorType = t === InteractionType.HydrogenBond ? FeatureType.HydrogenDonor : FeatureType.WeakHydrogenDonor;
|
||||
const isHydrogenDonorA = fA.types[fA.offsets[indexA]] === donorType;
|
||||
|
||||
eachBondedAtom(structure, uA, idxA, (u, idx) => {
|
||||
const eI = u.elements[idx];
|
||||
if (isHydrogen(structure, u, eI, 'polar')) {
|
||||
u.conformation.position(eI, p);
|
||||
const dist = Vec3.distance(p, pB);
|
||||
if (dist < minDistA) {
|
||||
minDistA = dist;
|
||||
Vec3.copy(posA, p);
|
||||
if (isHydrogenDonorA) {
|
||||
eachBondedAtom(structure, uA, idxA, (u, idx) => {
|
||||
const eI = u.elements[idx];
|
||||
if (isHydrogen(structure, u, eI, 'all')) {
|
||||
u.conformation.position(eI, p);
|
||||
const dist = Vec3.distance(p, pB);
|
||||
if (dist < minDistA) {
|
||||
minDistA = dist;
|
||||
Vec3.copy(posA, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eachBondedAtom(structure, uB, idxB, (u, idx) => {
|
||||
const eI = u.elements[idx];
|
||||
if (isHydrogen(structure, u, eI, 'polar')) {
|
||||
u.conformation.position(eI, p);
|
||||
const dist = Vec3.distance(p, pA);
|
||||
if (dist < minDistB) {
|
||||
minDistB = dist;
|
||||
Vec3.copy(posB, p);
|
||||
});
|
||||
} else {
|
||||
eachBondedAtom(structure, uB, idxB, (u, idx) => {
|
||||
const eI = u.elements[idx];
|
||||
if (isHydrogen(structure, u, eI, 'all')) {
|
||||
u.conformation.position(eI, p);
|
||||
const dist = Vec3.distance(p, pA);
|
||||
if (dist < minDistB) {
|
||||
minDistB = dist;
|
||||
Vec3.copy(posB, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Vec3.set(posA, fA.x[indexA], fA.y[indexA], fA.z[indexA]);
|
||||
Vec3.transformMat4(posA, posA, uA.conformation.operator.matrix);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Paul Pillot <paul.pillot@tandemai.com>
|
||||
*/
|
||||
|
||||
import { Unit, Structure, StructureElement } from '../../../mol-model/structure';
|
||||
@@ -19,7 +20,7 @@ import { UnitsMeshParams, UnitsVisual, UnitsMeshVisual } from '../../../mol-repr
|
||||
import { VisualUpdateState } from '../../../mol-repr/util';
|
||||
import { LocationIterator } from '../../../mol-geo/util/location-iterator';
|
||||
import { Interactions } from '../interactions/interactions';
|
||||
import { InteractionFlag } from '../interactions/common';
|
||||
import { FeatureType, InteractionFlag } from '../interactions/common';
|
||||
import { Sphere3D } from '../../../mol-math/geometry';
|
||||
import { StructureGroup, isHydrogen } from '../../../mol-repr/structure/visual/util/common';
|
||||
import { assertUnreachable } from '../../../mol-util/type-helpers';
|
||||
@@ -40,7 +41,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
|
||||
const features = interactions.unitsFeatures.get(unit.id);
|
||||
const contacts = interactions.unitsContacts.get(unit.id);
|
||||
|
||||
const { x, y, z, members, offsets } = features;
|
||||
const { x, y, z, members, offsets, types } = features;
|
||||
const { edgeCount, a, b, edgeProps: { flag, type } } = contacts;
|
||||
const { sizeFactor, ignoreHydrogens, ignoreHydrogensVariant, parentDisplay } = props;
|
||||
|
||||
@@ -56,7 +57,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
|
||||
position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
|
||||
const t = type[edgeIndex];
|
||||
if ((!ignoreHydrogens || ignoreHydrogensVariant !== 'all') && (
|
||||
t === InteractionType.HydrogenBond || t === InteractionType.WeakHydrogenBond)
|
||||
t === InteractionType.HydrogenBond || (t === InteractionType.WeakHydrogenBond && ignoreHydrogensVariant !== 'non-polar'))
|
||||
) {
|
||||
const idxA = members[offsets[a[edgeIndex]]];
|
||||
const idxB = members[offsets[b[edgeIndex]]];
|
||||
@@ -66,28 +67,32 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
|
||||
let minDistB = minDistA;
|
||||
Vec3.copy(posA, pA);
|
||||
Vec3.copy(posB, pB);
|
||||
const donorType = t === InteractionType.HydrogenBond ? FeatureType.HydrogenDonor : FeatureType.WeakHydrogenDonor;
|
||||
const isHydrogenDonorA = types[offsets[a[edgeIndex]]] === donorType;
|
||||
|
||||
eachIntraBondedAtom(unit, idxA, (_, idx) => {
|
||||
if (isHydrogen(structure, unit, elements[idx], 'polar')) {
|
||||
c.invariantPosition(elements[idx], p);
|
||||
const dist = Vec3.distance(p, pB);
|
||||
if (dist < minDistA) {
|
||||
minDistA = dist;
|
||||
Vec3.copy(posA, p);
|
||||
if (isHydrogenDonorA) {
|
||||
eachIntraBondedAtom(unit, idxA, (_, idx) => {
|
||||
if (isHydrogen(structure, unit, elements[idx], 'all')) {
|
||||
c.invariantPosition(elements[idx], p);
|
||||
const dist = Vec3.distance(p, pB);
|
||||
if (dist < minDistA) {
|
||||
minDistA = dist;
|
||||
Vec3.copy(posA, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
eachIntraBondedAtom(unit, idxB, (_, idx) => {
|
||||
if (isHydrogen(structure, unit, elements[idx], 'polar')) {
|
||||
c.invariantPosition(elements[idx], p);
|
||||
const dist = Vec3.distance(p, pA);
|
||||
if (dist < minDistB) {
|
||||
minDistB = dist;
|
||||
Vec3.copy(posB, p);
|
||||
});
|
||||
} else {
|
||||
eachIntraBondedAtom(unit, idxB, (_, idx) => {
|
||||
if (isHydrogen(structure, unit, elements[idx], 'all')) {
|
||||
c.invariantPosition(elements[idx], p);
|
||||
const dist = Vec3.distance(p, pA);
|
||||
if (dist < minDistB) {
|
||||
minDistB = dist;
|
||||
Vec3.copy(posB, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
} else {
|
||||
Vec3.set(posA, x[a[edgeIndex]], y[a[edgeIndex]], z[a[edgeIndex]]);
|
||||
Vec3.set(posB, x[b[edgeIndex]], y[b[edgeIndex]], z[b[edgeIndex]]);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) 2019-2020 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 { Structure } from '../../mol-model/structure';
|
||||
import { DSSPComputationParams, DSSPComputationProps, computeUnitDSSP } from './secondary-structure/dssp';
|
||||
import { DSSPComputationParams, DSSPComputationProps, DefaultDSSPComputationProps, computeUnitDSSP } from './secondary-structure/dssp';
|
||||
import { SecondaryStructure } from '../../mol-model/structure/model/properties/secondary-structure';
|
||||
import { ParamDefinition as PD } from '../../mol-util/param-definition';
|
||||
import { Unit } from '../../mol-model/structure/structure';
|
||||
@@ -14,26 +14,16 @@ import { CustomProperty } from '../common/custom-property';
|
||||
import { ModelSecondaryStructure } from '../../mol-model-formats/structure/property/secondary-structure';
|
||||
import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
|
||||
import { Model } from '../../mol-model/structure/model';
|
||||
import { computeUnitZhangSkolnik } from './secondary-structure/zhang-skolnik';
|
||||
|
||||
function getSecondaryStructureParams(data?: Structure) {
|
||||
let defaultType = 'model' as 'model' | 'dssp';
|
||||
if (data) {
|
||||
defaultType = 'dssp';
|
||||
for (let i = 0, il = data.models.length; i < il; ++i) {
|
||||
const m = data.models[i];
|
||||
if (Model.isFromPdbArchive(m) || Model.hasSecondaryStructure(m)) {
|
||||
// if there is any secondary structure definition given or if there is
|
||||
// an archival model, don't calculate dssp by default
|
||||
defaultType = 'model';
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
function getSecondaryStructureParams(_data?: Structure) {
|
||||
return {
|
||||
type: PD.MappedStatic(defaultType, {
|
||||
type: PD.MappedStatic('auto', {
|
||||
'auto': PD.EmptyGroup({ label: 'Automatic' }),
|
||||
'model': PD.EmptyGroup({ label: 'Model' }),
|
||||
'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true })
|
||||
}, { options: [['model', 'Model'], ['dssp', 'DSSP']] })
|
||||
'dssp': PD.Group(DSSPComputationParams, { label: 'DSSP', isFlat: true }),
|
||||
'zhang-skolnick': PD.EmptyGroup({ label: 'Zhang-Skolnick' }),
|
||||
}, { options: [['auto', 'Automatic'], ['model', 'Model'], ['dssp', 'DSSP'], ['zhang-skolnick', 'Zhang-Skolnick']] })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -57,15 +47,35 @@ export const SecondaryStructureProvider: CustomStructureProperty.Provider<Second
|
||||
obtain: async (ctx: CustomProperty.Context, data: Structure, props: Partial<SecondaryStructureProps>) => {
|
||||
const p = { ...PD.getDefaultValues(SecondaryStructureParams), ...props };
|
||||
switch (p.type.name) {
|
||||
case 'auto': return { value: await computeAuto(data) };
|
||||
case 'dssp': return { value: await computeDssp(data, p.type.params) };
|
||||
case 'model': return { value: await computeModel(data) };
|
||||
case 'zhang-skolnick': return { value: await computeZhangSkolnik(data) };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
async function computeAuto(structure: Structure): Promise<SecondaryStructureValue> {
|
||||
const map = new Map<number, SecondaryStructure>();
|
||||
for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
|
||||
const u = structure.unitSymmetryGroups[i].units[0];
|
||||
const m = u.model;
|
||||
if ((Model.isFromPdbArchive(m) && Model.isExperimental(m) && !Model.isCoarseGrained(m)) || Model.hasSecondaryStructure(m)) {
|
||||
const secondaryStructure = ModelSecondaryStructure.Provider.get(m);
|
||||
if (secondaryStructure) map.set(u.invariantId, secondaryStructure);
|
||||
} else if (Unit.isAtomic(u) && !Model.isCoarseGrained(m)) {
|
||||
const secondaryStructure = await computeUnitDSSP(u, DefaultDSSPComputationProps);
|
||||
map.set(u.invariantId, secondaryStructure);
|
||||
} else if (Unit.isAtomic(u)) {
|
||||
const secondaryStructure = await computeUnitZhangSkolnik(u);
|
||||
map.set(u.invariantId, secondaryStructure);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function computeDssp(structure: Structure, props: DSSPComputationProps): Promise<SecondaryStructureValue> {
|
||||
// TODO take inter-unit hbonds into account for bridge, ladder, sheet assignment
|
||||
// TODO use Zhang-Skolnik for CA alpha only parts or for coarse parts with per-residue elements
|
||||
const map = new Map<number, SecondaryStructure>();
|
||||
for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
|
||||
const u = structure.unitSymmetryGroups[i].units[0];
|
||||
@@ -77,15 +87,25 @@ async function computeDssp(structure: Structure, props: DSSPComputationProps): P
|
||||
return map;
|
||||
}
|
||||
|
||||
async function computeZhangSkolnik(structure: Structure): Promise<SecondaryStructureValue> {
|
||||
const map = new Map<number, SecondaryStructure>();
|
||||
for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
|
||||
const u = structure.unitSymmetryGroups[i].units[0];
|
||||
if (Unit.isAtomic(u)) {
|
||||
const secondaryStructure = await computeUnitZhangSkolnik(u);
|
||||
map.set(u.invariantId, secondaryStructure);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
async function computeModel(structure: Structure): Promise<SecondaryStructureValue> {
|
||||
const map = new Map<number, SecondaryStructure>();
|
||||
for (let i = 0, il = structure.unitSymmetryGroups.length; i < il; ++i) {
|
||||
const u = structure.unitSymmetryGroups[i].units[0];
|
||||
if (Unit.isAtomic(u)) {
|
||||
const secondaryStructure = ModelSecondaryStructure.Provider.get(u.model);
|
||||
if (secondaryStructure) {
|
||||
map.set(u.invariantId, secondaryStructure);
|
||||
}
|
||||
if (secondaryStructure) map.set(u.invariantId, secondaryStructure);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
* Copyright (c) 2019-2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author Sebastian Bittrich <sebastian.bittrich@rcsb.org>
|
||||
@@ -37,6 +37,7 @@ export const DSSPComputationParams = {
|
||||
};
|
||||
export type DSSPComputationParams = typeof DSSPComputationParams
|
||||
export type DSSPComputationProps = PD.Values<DSSPComputationParams>
|
||||
export const DefaultDSSPComputationProps = PD.getDefaultValues(DSSPComputationParams);
|
||||
|
||||
export async function computeUnitDSSP(unit: Unit.Atomic, params: DSSPComputationProps): Promise<SecondaryStructure> {
|
||||
const proteinInfo = getUnitProteinInfo(unit);
|
||||
|
||||
@@ -1 +1,116 @@
|
||||
// TODO
|
||||
/**
|
||||
* Copyright (c) 2024 mol* contributors, licensed under MIT, See LICENSE file for more info.
|
||||
*
|
||||
* @author Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
*/
|
||||
|
||||
import { SecondaryStructure } from '../../../mol-model/structure/model/properties/secondary-structure';
|
||||
import { SecondaryStructureType } from '../../../mol-model/structure/model/types';
|
||||
import { Unit } from '../../../mol-model/structure';
|
||||
import { ElementIndex, ResidueIndex } from '../../../mol-model/structure/model';
|
||||
import { SortedArray } from '../../../mol-data/int';
|
||||
import { Vec3 } from '../../../mol-math/linear-algebra/3d/vec3';
|
||||
|
||||
const HelixDistances = [5.45, 5.18, 6.37];
|
||||
const HelixDelta = 2.1;
|
||||
|
||||
const SheetDistances = [6.1, 10.4, 13.0];
|
||||
const SheetDelta = 1.42;
|
||||
|
||||
const posA = Vec3();
|
||||
const posB = Vec3();
|
||||
|
||||
function zhangSkolnickAtomicSS(unit: Unit.Atomic, residueIndices: SortedArray<ResidueIndex>, i: number, distances: number[], delta: number) {
|
||||
const c = unit.conformation;
|
||||
const { traceElementIndex } = unit.model.atomicHierarchy.derived.residue;
|
||||
|
||||
for (let j = Math.max(0, i - 2); j <= i; ++j) {
|
||||
for (let k = 2; k < 5; ++k) {
|
||||
if (j + k >= residueIndices.length) return false;
|
||||
|
||||
const rA = residueIndices[j];
|
||||
const rB = residueIndices[j + k];
|
||||
|
||||
const aA = traceElementIndex[rA];
|
||||
const aB = traceElementIndex[rB];
|
||||
if (aA === -1 || aB === -1) return false;
|
||||
|
||||
c.invariantPosition(aA as ElementIndex, posA);
|
||||
c.invariantPosition(aB as ElementIndex, posB);
|
||||
const d = Vec3.distance(posA, posB);
|
||||
|
||||
if (Math.abs(d - distances[k - 2]) >= delta) return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Secondary-structure assignment based on Zhang and Skolnick's TM-align paper.
|
||||
* TM-align: a protein structure alignment algorithm based on the Tm-score (2005) NAR, 33(7) 2302-2309.
|
||||
*
|
||||
* While not as accurate as DSSP, it is faster and works for coarse-grained/backbone-only models.
|
||||
*/
|
||||
export async function computeUnitZhangSkolnik(unit: Unit.Atomic): Promise<SecondaryStructure> {
|
||||
const count = unit.proteinElements.length;
|
||||
const type = new Uint32Array(count) as unknown as SecondaryStructureType[];
|
||||
const keys: number[] = [];
|
||||
const elements: SecondaryStructure.Element[] = [];
|
||||
|
||||
const { proteinElements, residueIndex } = unit;
|
||||
const residueCount = proteinElements.length;
|
||||
const unitProteinResidues = new Uint32Array(residueCount);
|
||||
for (let i = 0; i < residueCount; ++i) {
|
||||
const rI = residueIndex[proteinElements[i]];
|
||||
unitProteinResidues[i] = rI;
|
||||
}
|
||||
const residueIndices = SortedArray.ofSortedArray<ResidueIndex>(unitProteinResidues);
|
||||
const getIndex = (rI: ResidueIndex) => SortedArray.indexOf(residueIndices, rI);
|
||||
|
||||
for (let i = 0, il = residueIndices.length; i < il; ++i) {
|
||||
let flag = SecondaryStructureType.Flag.None;
|
||||
if (zhangSkolnickAtomicSS(unit, residueIndices, i, HelixDistances, HelixDelta)) {
|
||||
flag = SecondaryStructureType.Flag.Helix;
|
||||
} else if (zhangSkolnickAtomicSS(unit, residueIndices, i, SheetDistances, SheetDelta)) {
|
||||
flag = SecondaryStructureType.Flag.Beta;
|
||||
}
|
||||
type[i] = flag;
|
||||
if (elements.length === 0 || flag !== getFlag(elements[elements.length - 1])) {
|
||||
elements[elements.length] = createElement(mapToKind(flag), flag);
|
||||
}
|
||||
keys[i] = elements.length - 1;
|
||||
}
|
||||
|
||||
return SecondaryStructure(type, keys, elements, getIndex);
|
||||
}
|
||||
|
||||
function createElement(kind: string, flag: SecondaryStructureType.Flag): SecondaryStructure.Element {
|
||||
if (kind === 'helix') {
|
||||
return { kind: 'helix', flags: flag } as SecondaryStructure.Helix;
|
||||
} else if (kind === 'sheet') {
|
||||
return { kind: 'sheet', flags: flag } as SecondaryStructure.Sheet;
|
||||
} else {
|
||||
return { kind: 'none' };
|
||||
}
|
||||
}
|
||||
|
||||
function mapToKind(flag: SecondaryStructureType.Flag) {
|
||||
if (flag === SecondaryStructureType.Flag.Helix) {
|
||||
return 'helix';
|
||||
} else if (flag === SecondaryStructureType.Flag.Beta) {
|
||||
return 'sheet';
|
||||
} else {
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function getFlag(element: SecondaryStructure.Element) {
|
||||
if (element.kind === 'helix') {
|
||||
return element.flags;
|
||||
} else if (element.kind === 'sheet') {
|
||||
return element.flags;
|
||||
} else {
|
||||
return SecondaryStructureType.Flag.None;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2023 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 Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
@@ -251,16 +251,22 @@ export namespace Model {
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Has typical coarse grained atom names (BB, SC1) or less than three times as many
|
||||
* atoms as polymer residues (C-alpha only models).
|
||||
* Mark as coarse grained if any of the following conditions are met:
|
||||
* - has typical coarse grained atom names (BB, SC1)
|
||||
* - has less than three times as many atoms as polymer residues (C-alpha only models)
|
||||
* - has no standard sidechain atoms
|
||||
*/
|
||||
export function isCoarseGrained(model: Model): boolean {
|
||||
let coarseGrained = CoarseGrained.get(model);
|
||||
if (coarseGrained === undefined) {
|
||||
let polymerResidueCount = 0;
|
||||
const { polymerType } = model.atomicHierarchy.derived.residue;
|
||||
let polymerDirectionCount = 0;
|
||||
const { polymerType, directionToElementIndex } = model.atomicHierarchy.derived.residue;
|
||||
for (let i = 0; i < polymerType.length; ++i) {
|
||||
if (polymerType[i] !== PolymerType.NA) polymerResidueCount += 1;
|
||||
if (polymerType[i] !== PolymerType.NA) {
|
||||
polymerResidueCount += 1;
|
||||
if (directionToElementIndex[i] !== -1) polymerDirectionCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// check for coarse grained atom names
|
||||
@@ -273,11 +279,16 @@ export namespace Model {
|
||||
if (hasBB && hasSC1) break;
|
||||
}
|
||||
|
||||
coarseGrained = (hasBB && hasSC1) || (
|
||||
polymerResidueCount && atomCount
|
||||
? atomCount / polymerResidueCount < 3
|
||||
: false
|
||||
);
|
||||
coarseGrained = false;
|
||||
if (atomCount > 0 && polymerResidueCount > 0) {
|
||||
if (hasBB && hasSC1) {
|
||||
coarseGrained = true;
|
||||
} else if (atomCount / polymerResidueCount < 3) {
|
||||
coarseGrained = true;
|
||||
} else if (polymerDirectionCount === 0) {
|
||||
coarseGrained = true;
|
||||
}
|
||||
}
|
||||
CoarseGrained.set(model, coarseGrained);
|
||||
}
|
||||
return coarseGrained;
|
||||
@@ -378,6 +389,33 @@ export namespace Model {
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isExperimental(model: Model): boolean {
|
||||
if (!MmcifFormat.is(model.sourceData)) return false;
|
||||
const { db } = model.sourceData.data;
|
||||
for (let i = 0; i < db.struct.pdbx_structure_determination_methodology.rowCount; i++) {
|
||||
if (db.struct.pdbx_structure_determination_methodology.value(i).toLowerCase() === 'experimental') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isIntegrative(model: Model): boolean {
|
||||
if (!MmcifFormat.is(model.sourceData)) return false;
|
||||
const { db } = model.sourceData.data;
|
||||
for (let i = 0; i < db.struct.pdbx_structure_determination_methodology.rowCount; i++) {
|
||||
if (db.struct.pdbx_structure_determination_methodology.value(i).toLowerCase() === 'integrative') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isComputational(model: Model): boolean {
|
||||
if (!MmcifFormat.is(model.sourceData)) return false;
|
||||
const { db } = model.sourceData.data;
|
||||
for (let i = 0; i < db.struct.pdbx_structure_determination_methodology.rowCount; i++) {
|
||||
if (db.struct.pdbx_structure_determination_methodology.value(i).toLowerCase() === 'computational') return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function hasXrayMap(model: Model): boolean {
|
||||
if (!MmcifFormat.is(model.sourceData)) return false;
|
||||
// Check exprimental method to exclude models solved with
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Copyright (c) 2017-2022 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 Alexander Rose <alexander.rose@weirdbyte.de>
|
||||
* @author David Sehnal <david.sehnal@gmail.com>
|
||||
@@ -103,8 +103,8 @@ export const PolymerTypeAtomRoleId: { [k in PolymerType]: { [k in AtomRole]: Set
|
||||
backboneStart: new Set(['N']),
|
||||
backboneEnd: new Set(['C']),
|
||||
// CA1 is used e.g. in GFP chromophores
|
||||
// BB is often used for coarse grained models
|
||||
coarseBackbone: new Set(['CA', 'BB', 'CA1'])
|
||||
// BB, BAS are often used for coarse grained models
|
||||
coarseBackbone: new Set(['CA', 'CA1', 'BB', 'BAS'])
|
||||
},
|
||||
[PolymerType.GammaProtein]: {
|
||||
trace: new Set(['CA']),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -139,7 +139,7 @@ function atomGroupsSegmented({ unitTest, entityTest, chainTest, residueTest, ato
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
|
||||
const { chainElementSegments } = unit.kind === Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
|
||||
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
|
||||
|
||||
while (chainsIt.hasNext) {
|
||||
@@ -213,7 +213,7 @@ function atomGroupsGrouped({ unitTest, entityTest, chainTest, residueTest, atomT
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const { chainElementSegments } = Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
|
||||
const { chainElementSegments } = unit.kind === Unit.Kind.Spheres ? model.coarseHierarchy.spheres : model.coarseHierarchy.gaussians;
|
||||
const chainsIt = Segmentation.transientSegments(chainElementSegments, elements);
|
||||
while (chainsIt.hasNext) {
|
||||
const chainSegment = chainsIt.move();
|
||||
|
||||
@@ -161,7 +161,7 @@ const entity = {
|
||||
pdbx_mutation: p(l => l.unit.model.entities.data.pdbx_mutation.value(eK(l))),
|
||||
pdbx_fragment: p(l => l.unit.model.entities.data.pdbx_fragment.value(eK(l))),
|
||||
pdbx_ec: p(l => l.unit.model.entities.data.pdbx_ec.value(eK(l))),
|
||||
|
||||
pdbx_parent_entity_id: p(l => l.unit.model.entities.data.pdbx_parent_entity_id.value(eK(l))),
|
||||
subtype: p(l => l.unit.model.entities.subtype.value(eK(l))),
|
||||
prd_id: p(l => l.unit.model.entities.prd_id?.value(eK(l)) ?? ''),
|
||||
};
|
||||
@@ -197,4 +197,4 @@ const StructureProperties = {
|
||||
};
|
||||
|
||||
type StructureProperties = typeof StructureProperties
|
||||
export { StructureProperties };
|
||||
export { StructureProperties };
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user