diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000000000000000000000000000000000000..787d4e89c18edd9fa1021145b148691833d09c9b
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,10 @@
+<!-- Thank you for contributing to Mol* -->
+
+# Description
+
+
+## Actions
+
+- [ ] Added description of changes to the `[Unreleased]` section of `CHANGELOG.md`
+- [ ] Updated headers of modified files
+- [ ] Added my name to `package.json`'s `contributors`
\ No newline at end of file
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
index 966b67347d36bc37436d3ba78525db8a5a4275fc..93c043ab7c334b348ea377d93146bf0506bebd6b 100644
--- a/.vscode/extensions.json
+++ b/.vscode/extensions.json
@@ -6,7 +6,6 @@
 	"recommendations": [
 		"dbaeumer.vscode-eslint",
 		"firsttris.vscode-jest-runner",
-		"msjsdiag.debugger-for-chrome",
 		"slevesque.shader",
 		"stpn.vscode-graphql",
 		"wayou.vscode-todo-highlight"
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 40b0426843bf46aaa4db84f662c14388229f14c6..5cef38d6f9c31c589e0dd89fa3ba0abbb53f3c11 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,127 @@ Note that since we don't clearly distinguish between a public and private interf
 
 ## [Unreleased]
 
+- Make `PluginContext.initContainer` checkered canvas background optional
+
+## [v3.23.0] - 2022-10-19
+
+- Add `PluginContext.initContainer/mount/unmount` methods; these should make it easier to reuse a plugin context with both custom and built-in UI
+- Add `PluginContext.canvas3dInitialized`
+- `createPluginUI` now resolves after the 3d canvas has been initialized
+- Change EM Volume Streaming default from `Whote Structure` to `Auto`
+
+## [v3.22.0] - 2022-10-17
+
+- Replace `VolumeIsosurfaceParams.pickingGranularity` param with `Volume.PickingGranuality` 
+
+## [v3.21.0] - 2022-10-17
+
+- Add `VolumeIsosurfaceParams.pickingGranularity` param
+- Prevent component controls collapsing when option is selected
+
+## [v3.20.0] - 2022-10-16
+
+- [Breaking] Rename the ``model-index`` color theme to ``trajectory-index``
+- Add a new ``model-index`` color theme that uniquely colors each loaded model
+- Add the new ``model-index`` and ``structure-index`` color themes as an option for the carbon color in the ``element-symbol`` and ``ilustrative`` color themes
+- Add ``structure-index`` color theme that uniquely colors each root structure
+- Add ``nearest`` method to ``Lookup3D``
+- Add mipmap-based blur for skybox backgrounds
+
+## [v3.19.0] - 2022-10-01
+
+- Fix "empty textures" error on empty canvas
+- Optimize BinaryCIF integer packing encoder
+- Fix dual depth peeling when post-processing is off or when rendering direct-volumes
+- Add ``cameraClipping.minNear`` parameter
+- Fix black artifacts on specular highlights with transparent background
+
+## [v3.18.0] - 2022-09-17
+
+- Integration of Dual depth peeling - OIT method
+- Stereo camera improvements
+    - Fix param updates not applied
+    - Better param ranges and description
+    - Add timer.mark for left/right camera
+
+## [v3.17.0] - 2022-09-11
+
+- [Fix] Clone ``Canvas3DParams`` when creating a ``Canvas3D`` instance to prevent shared state between multiple instances
+- Add ``includeResidueTest`` option to ``alignAndSuperposeWithSIFTSMapping``
+- Add ``parentDisplay`` param for interactions representation.
+- [Experimental] Add support for PyMOL, VMD, and Jmol atom expressions in selection scripts
+- Support for ``failIfMajorPerformanceCaveat`` webgl attribute. Add ``PluginConfig.General.AllowMajorPerformanceCaveat`` and ``allow-major-performance-caveat`` Viewer GET param.
+- Fix handling of PDB TER records (#549)
+- Add support for getting multiple loci from a representation (``.getAllLoci()``)
+- Add ``key`` property to intra- and inter-bonds for referencing source data
+- Fix click event triggered after move
+
+## [v3.16.0] - 2022-08-25
+
+- Support ``globalColorParams`` and ``globalSymmetryParams`` in common representation params
+- Support ``label`` parameter in ``Viewer.loadStructureFromUrl``
+- Fix ``ViewportHelpContent`` Mouse Controls section
+
+## [v3.15.0] - 2022-08-23
+
+- Fix wboit in Safari >=15 (add missing depth renderbuffer to wboit pass)
+- Add 'Around Camera' option to Volume streaming
+- Avoid queuing more than one update in Volume streaming
+
+## [v3.14.0] - 2022-08-20
+
+- Expose inter-bonds compute params in structure
+- Improve performance of inter/intra-bonds compute
+- Fix defaultAttribs handling in Canvas3DContext.fromCanvas
+- Confal pyramids extension improvements
+    - Add custom labels to Confal pyramids
+    - Improve naming of some internal types in Confal pyramids extension coordinate
+    - Add example mmCIF file with categories necessary to display Confal pyramids
+    - Change the lookup logic of NtC steps from residues
+- Add support for download of gzipped files
+- Don't filter IndexPairBonds by element-based rules in MOL/SDF and MOL2 (without symmetry) models
+- Fix Glycam Saccharide Names used by default
+- Fix GPU surfaces rendering in Safari with WebGL2
+- Add ``fov`` (Field of View) Canvas3D parameter
+- Add ``sceneRadiusFactor`` Canvas3D parameter
+- Add background pass (skybox, image, horizontal/radial gradient)
+    - Set simple-settings presets via ``PluginConfig.Background.Styles``
+    - Example presets in new backgrounds extension
+    - Load skybox/image from URL or File (saved in session)
+    - Opacity, saturation, lightness controls for skybox/image
+    - Coverage (viewport or canvas) controls for image/gradient
+- [Breaking] ``AssetManager`` needs to be passed to various graphics related classes
+- Fix SSAO renderable initialization
+- Reduce number of webgl state changes
+    - Add ``viewport`` and ``scissor`` to state object
+    - Add ``hasOpaque`` to scene object
+- Handle edge cases where some renderables would not get (correctly) rendered
+    - Fix text background rendering for opaque text
+    - Fix helper scenes not shown when rendering directly to draw target
+- Fix ``CustomElementProperty`` coloring not working
+
+## [v3.13.0] - 2022-07-24
+
+- Fix: only update camera state if manualReset is off (#494)
+- Improve handling principal axes of points in a plane
+- Add 'material' annotation support for textures
+- More effort to avoid using ``flat`` qualifier in shaders: add ``dVaryingGroup``
+- Enable ``immediateUpdate`` for iso level in isosurface and volume streaming controls
+- Add support to download CCD from configurable URL
+
+## [v3.12.1] - 2022-07-20
+
+- Fix plugin behavior dispose logic to correctly unsubscribe observables.
+
+## [v3.12.0] - 2022-07-17
+
+- Add ``colorMarker`` option to Renderer. This disables the highlight and select marker at a shader level for faster rendering of large scenes in some cases.
+- Bind shared textures only once per pass, not for each render item
+- Fix missing 'material' annotation for some uniforms, causing unnecessary uniform updates
+- Remove use of ``isnan`` in impostor shaders, not needed and causing slowdown
+- Avoid using ``flat`` qualifier in shaders, causing slowdown
+- Improve CellPack's ``adjustStyle`` option (disable ``colorMarker``, set component options, enable marking w/o ghost)
+- Scan all entities when looking for ``struct_conn`` entries (fixes issue when the same ``label_asym_id`` is used in more than one entity)
 
 ## [v3.11.0] - 2022-07-04
 
diff --git a/README.md b/README.md
index 10e926c509f71742f239fe3ccf7e064b7130e470..9125bac897959988a7bbb5405f62481fac94cf41 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ and navigate to `build/viewer`
 
 **GraphQL schemas**
 
-    node node_modules//@graphql-codegen/cli/bin -c src/extensions/rcsb/graphql/codegen.yml
+    node node_modules/@graphql-codegen/cli/cjs/bin -c src/extensions/rcsb/graphql/codegen.yml
 
 ### Other scripts
 **Create chem comp bond table**
@@ -152,7 +152,7 @@ Or
     node lib/commonjs/cli/cif2bcif
 
 E.g.
- 
+
     node lib/commonjs/cli/cif2bcif src.cif out.bcif.gz
     node lib/commonjs/cli/cif2bcif src.bcif.gz out.cif
 
diff --git a/examples/1bna_confal_pyramids.cif b/examples/1bna_confal_pyramids.cif
new file mode 100644
index 0000000000000000000000000000000000000000..c4ab21a1b4e4973acd3ee1de28231f73e25e5249
--- /dev/null
+++ b/examples/1bna_confal_pyramids.cif
@@ -0,0 +1,1694 @@
+data_1BNA
+#
+_entry.id  1BNA
+##
+_audit_conform.dict_name      mmcif_pdbx.dic
+_audit_conform.dict_version   5.279
+_audit_conform.dict_location  http://mmcif.pdb.org/dictionaries/ascii/mmcif_pdbx.dic
+##
+loop_
+_database_2.database_id
+_database_2.database_code
+PDB    1BNA          
+RCSB   BDL001        
+WWPDB  D_1000171933  
+##
+_pdbx_database_status.status_code                    REL
+_pdbx_database_status.entry_id                       1BNA
+_pdbx_database_status.recvd_initial_deposition_date  1981-01-26
+_pdbx_database_status.deposit_site                   BNL
+_pdbx_database_status.process_site                   BNL
+_pdbx_database_status.status_code_sf                 REL
+_pdbx_database_status.status_code_mr                 ?
+_pdbx_database_status.SG_entry                       ?
+_pdbx_database_status.pdb_format_compatible          Y
+_pdbx_database_status.status_code_cs                 ?
+##
+loop_
+_audit_author.name
+_audit_author.pdbx_ordinal
+'Drew, H.R.'       1  
+'Wing, R.M.'       2  
+'Takano, T.'       3  
+'Broka, C.'        4  
+'Tanaka, S.'       5  
+'Itakura, K.'      6  
+'Dickerson, R.E.'  7  
+##
+loop_
+_citation.id
+_citation.title
+_citation.journal_abbrev
+_citation.journal_volume
+_citation.page_first
+_citation.page_last
+_citation.year
+_citation.journal_id_ASTM
+_citation.country
+_citation.journal_id_ISSN
+_citation.journal_id_CSD
+_citation.book_publisher
+_citation.pdbx_database_id_PubMed
+_citation.pdbx_database_id_DOI
+primary  'Structure of a B-DNA dodecamer: conformation and dynamics.'                         Proc.Natl.Acad.Sci.USA   78  2179  2183  1981  PNASA6  US  0027-8424  0040  ?  6941276  10.1073/pnas.78.4.2179  
+1        'Kinematic Model for B-DNA'                                                          Proc.Natl.Acad.Sci.USA   78  7318  7322  1981  PNASA6  US  0027-8424  0040  ?        ?                       ?  
+2        'Structure of a B-DNA Dodecamer. II. Influence of Base Sequence on Helix Structure'  J.Mol.Biol.             149   761   786  1981  JMOBAK  UK  0022-2836  0070  ?        ?                       ?  
+3        'Structure of a B-DNA Dodecamer. III. Geometry of Hydration'                         J.Mol.Biol.             151   535   556  1981  JMOBAK  UK  0022-2836  0070  ?        ?                       ?  
+4        'Crystal Structure Analysis of a Complete Turn of B-DNA'                             Nature                  287   755   758  1980  NATUAS  UK  0028-0836  0006  ?        ?                       ?  
+##
+loop_
+_citation_author.citation_id
+_citation_author.name
+_citation_author.ordinal
+primary  'Drew, H.R.'        1  
+primary  'Wing, R.M.'        2  
+primary  'Takano, T.'        3  
+primary  'Broka, C.'         4  
+primary  'Tanaka, S.'        5  
+primary  'Itakura, K.'       6  
+primary  'Dickerson, R.E.'   7  
+1        'Dickerson, R.E.'   8  
+1        'Drew, H.R.'        9  
+2        'Dickerson, R.E.'  10  
+2        'Drew, H.R.'       11  
+3        'Drew, H.R.'       12  
+3        'Dickerson, R.E.'  13  
+4        'Wing, R.'         14  
+4        'Drew, H.R.'       15  
+4        'Takano, T.'       16  
+4        'Broka, C.'        17  
+4        'Tanaka, S.'       18  
+4        'Itakura, K.'      19  
+4        'Dickerson, R.E.'  20  
+##
+_cell.entry_id          1BNA
+_cell.length_a          24.870
+_cell.length_b          40.390
+_cell.length_c          66.200
+_cell.angle_alpha       90.00
+_cell.angle_beta        90.00
+_cell.angle_gamma       90.00
+_cell.Z_PDB             8
+_cell.pdbx_unique_axis  ?
+##
+_symmetry.entry_id                        1BNA
+_symmetry.space_group_name_H-M            'P 21 21 21'
+_symmetry.pdbx_full_space_group_name_H-M  ?
+_symmetry.cell_setting                    ?
+_symmetry.Int_Tables_number               19
+##
+loop_
+_entity.id
+_entity.type
+_entity.src_method
+_entity.pdbx_description
+_entity.formula_weight
+_entity.pdbx_number_of_molecules
+_entity.pdbx_ec
+_entity.pdbx_mutation
+_entity.pdbx_fragment
+_entity.details
+1  polymer  syn  "DNA(5'-D(*CP*GP*CP*GP*AP*AP*TP*TP*CP*GP*CP*G)-3')"  3663.392   2  ?  ?  ?  ?  
+2  water    nat  water                                                  18.015  80  ?  ?  ?  ?  
+##
+_entity_poly.entity_id                     1
+_entity_poly.type                          polydeoxyribonucleotide
+_entity_poly.nstd_linkage                  no
+_entity_poly.nstd_monomer                  no
+_entity_poly.pdbx_seq_one_letter_code      '(DC)(DG)(DC)(DG)(DA)(DA)(DT)(DT)(DC)(DG)(DC)(DG)'
+_entity_poly.pdbx_seq_one_letter_code_can  CGCGAATTCGCG
+_entity_poly.pdbx_strand_id                A,B
+_entity_poly.pdbx_target_identifier        ?
+##
+loop_
+_entity_poly_seq.entity_id
+_entity_poly_seq.num
+_entity_poly_seq.mon_id
+_entity_poly_seq.hetero
+1   1  DC  n  
+1   2  DG  n  
+1   3  DC  n  
+1   4  DG  n  
+1   5  DA  n  
+1   6  DA  n  
+1   7  DT  n  
+1   8  DT  n  
+1   9  DC  n  
+1  10  DG  n  
+1  11  DC  n  
+1  12  DG  n  
+##
+_struct_ref.id                        1
+_struct_ref.entity_id                 1
+_struct_ref.db_name                   PDB
+_struct_ref.db_code                   1BNA
+_struct_ref.pdbx_db_accession         1BNA
+_struct_ref.pdbx_db_isoform           ?
+_struct_ref.pdbx_seq_one_letter_code  ?
+_struct_ref.pdbx_align_begin          ?
+##
+loop_
+_struct_ref_seq.align_id
+_struct_ref_seq.ref_id
+_struct_ref_seq.pdbx_PDB_id_code
+_struct_ref_seq.pdbx_strand_id
+_struct_ref_seq.seq_align_beg
+_struct_ref_seq.pdbx_seq_align_beg_ins_code
+_struct_ref_seq.seq_align_end
+_struct_ref_seq.pdbx_seq_align_end_ins_code
+_struct_ref_seq.pdbx_db_accession
+_struct_ref_seq.db_align_beg
+_struct_ref_seq.pdbx_db_align_beg_ins_code
+_struct_ref_seq.db_align_end
+_struct_ref_seq.pdbx_db_align_end_ins_code
+_struct_ref_seq.pdbx_auth_seq_align_beg
+_struct_ref_seq.pdbx_auth_seq_align_end
+1  1  1BNA  A  1  ?  12  ?  1BNA   1  ?  12  ?   1  12  
+2  1  1BNA  B  1  ?  12  ?  1BNA  13  ?  24  ?  13  24  
+##
+loop_
+_chem_comp.id
+_chem_comp.type
+_chem_comp.mon_nstd_flag
+_chem_comp.name
+_chem_comp.pdbx_synonyms
+_chem_comp.formula
+_chem_comp.formula_weight
+DA   'DNA linking'  y  "2'-DEOXYADENOSINE-5'-MONOPHOSPHATE"  ?  'C10 H14 N5 O6 P'  331.222  
+DC   'DNA linking'  y  "2'-DEOXYCYTIDINE-5'-MONOPHOSPHATE"   ?  'C9 H14 N3 O7 P'   307.197  
+DG   'DNA linking'  y  "2'-DEOXYGUANOSINE-5'-MONOPHOSPHATE"  ?  'C10 H14 N5 O7 P'  347.221  
+DT   'DNA linking'  y  "THYMIDINE-5'-MONOPHOSPHATE"          ?  'C10 H15 N2 O8 P'  322.208  
+HOH  non-polymer    .  WATER                                 ?  'H2 O'              18.015  
+##
+_exptl.entry_id         1BNA
+_exptl.method           'X-RAY DIFFRACTION'
+_exptl.crystals_number  ?
+##
+_exptl_crystal.id                   1
+_exptl_crystal.density_meas         ?
+_exptl_crystal.density_Matthews     2.27
+_exptl_crystal.density_percent_sol  45.79
+_exptl_crystal.description          ?
+##
+_exptl_crystal_grow.crystal_id     1
+_exptl_crystal_grow.method         'VAPOR DIFFUSION'
+_exptl_crystal_grow.temp           290.00
+_exptl_crystal_grow.temp_details   ?
+_exptl_crystal_grow.pH             ?
+_exptl_crystal_grow.pdbx_details   'VAPOR DIFFUSION, temperature 290.00K'
+_exptl_crystal_grow.pdbx_pH_range  ?
+##
+loop_
+_exptl_crystal_grow_comp.crystal_id
+_exptl_crystal_grow_comp.id
+_exptl_crystal_grow_comp.sol_id
+_exptl_crystal_grow_comp.name
+_exptl_crystal_grow_comp.volume
+_exptl_crystal_grow_comp.conc
+_exptl_crystal_grow_comp.details
+1  1  1  WATER         ?  ?  ?  
+1  2  1  'MG ACETATE'  ?  ?  ?  
+1  3  1  SPERMINE_HCL  ?  ?  ?  
+1  4  2  WATER         ?  ?  ?  
+1  5  2  MPD           ?  ?  ?  
+##
+_diffrn.id                    1
+_diffrn.crystal_id            1
+_diffrn.ambient_temp          ?
+_diffrn.ambient_temp_details  ?
+##
+_diffrn_detector.diffrn_id             1
+_diffrn_detector.detector              DIFFRACTOMETER
+_diffrn_detector.type                  ?
+_diffrn_detector.pdbx_collection_date  ?
+_diffrn_detector.details               ?
+##
+_diffrn_radiation.diffrn_id                       1
+_diffrn_radiation.wavelength_id                   1
+_diffrn_radiation.pdbx_monochromatic_or_laue_m_l  ?
+_diffrn_radiation.monochromator                   ?
+_diffrn_radiation.pdbx_diffrn_protocol            ?
+_diffrn_radiation.pdbx_scattering_type            x-ray
+##
+_diffrn_radiation_wavelength.id          1
+_diffrn_radiation_wavelength.wavelength  .
+_diffrn_radiation_wavelength.wt          1.0
+##
+_diffrn_source.diffrn_id                  1
+_diffrn_source.source                     ?
+_diffrn_source.type                       ?
+_diffrn_source.pdbx_synchrotron_site      ?
+_diffrn_source.pdbx_synchrotron_beamline  ?
+_diffrn_source.pdbx_wavelength            ?
+_diffrn_source.pdbx_wavelength_list       ?
+##
+_reflns.entry_id                    1BNA
+_reflns.observed_criterion_sigma_I  ?
+_reflns.observed_criterion_sigma_F  ?
+_reflns.d_resolution_low            8.0
+_reflns.d_resolution_high           1.900
+_reflns.number_obs                  5534
+_reflns.number_all                  ?
+_reflns.percent_possible_obs        ?
+_reflns.pdbx_Rmerge_I_obs           ?
+_reflns.pdbx_Rsym_value             ?
+_reflns.pdbx_netI_over_sigmaI       ?
+_reflns.B_iso_Wilson_estimate       ?
+_reflns.pdbx_redundancy             ?
+_reflns.pdbx_diffrn_id              1
+_reflns.pdbx_ordinal                1
+##
+_refine.entry_id                                1BNA
+_refine.ls_number_reflns_obs                    2725
+_refine.ls_number_reflns_all                    ?
+_refine.pdbx_ls_sigma_I                         2.000
+_refine.pdbx_ls_sigma_F                         ?
+_refine.pdbx_data_cutoff_high_absF              ?
+_refine.pdbx_data_cutoff_low_absF               ?
+_refine.pdbx_data_cutoff_high_rms_absF          ?
+_refine.ls_d_res_low                            8.000
+_refine.ls_d_res_high                           1.900
+_refine.ls_percent_reflns_obs                   ?
+_refine.ls_R_factor_obs                         0.1780000
+_refine.ls_R_factor_all                         ?
+_refine.ls_R_factor_R_work                      ?
+_refine.ls_R_factor_R_free                      ?
+_refine.ls_R_factor_R_free_error                ?
+_refine.ls_R_factor_R_free_error_details        ?
+_refine.ls_percent_reflns_R_free                ?
+_refine.ls_number_reflns_R_free                 ?
+_refine.ls_number_parameters                    ?
+_refine.ls_number_restraints                    ?
+_refine.occupancy_min                           ?
+_refine.occupancy_max                           ?
+_refine.B_iso_mean                              ?
+_refine.aniso_B[1][1]                           ?
+_refine.aniso_B[2][2]                           ?
+_refine.aniso_B[3][3]                           ?
+_refine.aniso_B[1][2]                           ?
+_refine.aniso_B[1][3]                           ?
+_refine.aniso_B[2][3]                           ?
+_refine.solvent_model_details                   ?
+_refine.solvent_model_param_ksol                ?
+_refine.solvent_model_param_bsol                ?
+_refine.pdbx_ls_cross_valid_method              ?
+_refine.details                                 ?
+_refine.pdbx_starting_model                     ?
+_refine.pdbx_method_to_determine_struct         ?
+_refine.pdbx_isotropic_thermal_model            ?
+_refine.pdbx_stereochemistry_target_values      ?
+_refine.pdbx_stereochem_target_val_spec_case    ?
+_refine.pdbx_R_Free_selection_details           ?
+_refine.pdbx_overall_ESU_R                      ?
+_refine.pdbx_overall_ESU_R_Free                 ?
+_refine.overall_SU_ML                           ?
+_refine.overall_SU_B                            ?
+_refine.pdbx_refine_id                          'X-RAY DIFFRACTION'
+_refine.pdbx_diffrn_id                          1
+_refine.pdbx_TLS_residual_ADP_flag              ?
+_refine.correlation_coeff_Fo_to_Fc              ?
+_refine.correlation_coeff_Fo_to_Fc_free         ?
+_refine.pdbx_solvent_vdw_probe_radii            ?
+_refine.pdbx_solvent_ion_probe_radii            ?
+_refine.pdbx_solvent_shrinkage_radii            ?
+_refine.pdbx_overall_phase_error                ?
+_refine.overall_SU_R_Cruickshank_DPI            ?
+_refine.pdbx_overall_SU_R_free_Cruickshank_DPI  ?
+_refine.pdbx_overall_SU_R_Blow_DPI              ?
+_refine.pdbx_overall_SU_R_free_Blow_DPI         ?
+##
+_refine_hist.pdbx_refine_id                  'X-RAY DIFFRACTION'
+_refine_hist.cycle_id                        LAST
+_refine_hist.pdbx_number_atoms_protein       0
+_refine_hist.pdbx_number_atoms_nucleic_acid  486
+_refine_hist.pdbx_number_atoms_ligand        0
+_refine_hist.number_atoms_solvent            80
+_refine_hist.number_atoms_total              566
+_refine_hist.d_res_high                      1.900
+_refine_hist.d_res_low                       8.000
+##
+_struct.entry_id                 1BNA
+_struct.title                    'STRUCTURE OF A B-DNA DODECAMER. CONFORMATION AND DYNAMICS'
+_struct.pdbx_descriptor          "5'-D(*CP*GP*CP*GP*AP*AP*TP*TP*CP*GP*CP*G)-3',290 K"
+_struct.pdbx_model_details       ?
+_struct.pdbx_CASP_flag           ?
+_struct.pdbx_model_type_details  ?
+##
+_struct_keywords.entry_id       1BNA
+_struct_keywords.pdbx_keywords  DNA
+_struct_keywords.text           'B-DNA, DOUBLE HELIX, DNA'
+##
+loop_
+_struct_asym.id
+_struct_asym.pdbx_blank_PDB_chainid_flag
+_struct_asym.pdbx_modified
+_struct_asym.entity_id
+_struct_asym.details
+A  N  N  1  ?  
+B  N  N  1  ?  
+C  N  N  2  ?  
+D  N  N  2  ?  
+##
+_struct_biol.id  1
+##
+loop_
+_struct_conn.id
+_struct_conn.conn_type_id
+_struct_conn.pdbx_leaving_atom_flag
+_struct_conn.pdbx_PDB_id
+_struct_conn.ptnr1_label_asym_id
+_struct_conn.ptnr1_label_comp_id
+_struct_conn.ptnr1_label_seq_id
+_struct_conn.ptnr1_label_atom_id
+_struct_conn.pdbx_ptnr1_label_alt_id
+_struct_conn.pdbx_ptnr1_PDB_ins_code
+_struct_conn.pdbx_ptnr1_standard_comp_id
+_struct_conn.ptnr1_symmetry
+_struct_conn.ptnr2_label_asym_id
+_struct_conn.ptnr2_label_comp_id
+_struct_conn.ptnr2_label_seq_id
+_struct_conn.ptnr2_label_atom_id
+_struct_conn.pdbx_ptnr2_label_alt_id
+_struct_conn.pdbx_ptnr2_PDB_ins_code
+_struct_conn.ptnr1_auth_asym_id
+_struct_conn.ptnr1_auth_comp_id
+_struct_conn.ptnr1_auth_seq_id
+_struct_conn.ptnr2_auth_asym_id
+_struct_conn.ptnr2_auth_comp_id
+_struct_conn.ptnr2_auth_seq_id
+_struct_conn.ptnr2_symmetry
+_struct_conn.pdbx_ptnr3_label_atom_id
+_struct_conn.pdbx_ptnr3_label_seq_id
+_struct_conn.pdbx_ptnr3_label_comp_id
+_struct_conn.pdbx_ptnr3_label_asym_id
+_struct_conn.pdbx_ptnr3_label_alt_id
+_struct_conn.pdbx_ptnr3_PDB_ins_code
+_struct_conn.details
+_struct_conn.pdbx_dist_value
+_struct_conn.pdbx_value_order
+hydrog1   hydrog  ?  ?  A  DC   1  N3  ?  ?  ?  1_555  B  DG  12  N1  ?  ?  A  DC   1  B  DG  24  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog2   hydrog  ?  ?  A  DC   1  N4  ?  ?  ?  1_555  B  DG  12  O6  ?  ?  A  DC   1  B  DG  24  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog3   hydrog  ?  ?  A  DC   1  O2  ?  ?  ?  1_555  B  DG  12  N2  ?  ?  A  DC   1  B  DG  24  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog4   hydrog  ?  ?  A  DG   2  N1  ?  ?  ?  1_555  B  DC  11  N3  ?  ?  A  DG   2  B  DC  23  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog5   hydrog  ?  ?  A  DG   2  N2  ?  ?  ?  1_555  B  DC  11  O2  ?  ?  A  DG   2  B  DC  23  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog6   hydrog  ?  ?  A  DG   2  O6  ?  ?  ?  1_555  B  DC  11  N4  ?  ?  A  DG   2  B  DC  23  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog7   hydrog  ?  ?  A  DC   3  N3  ?  ?  ?  1_555  B  DG  10  N1  ?  ?  A  DC   3  B  DG  22  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog8   hydrog  ?  ?  A  DC   3  N4  ?  ?  ?  1_555  B  DG  10  O6  ?  ?  A  DC   3  B  DG  22  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog9   hydrog  ?  ?  A  DC   3  O2  ?  ?  ?  1_555  B  DG  10  N2  ?  ?  A  DC   3  B  DG  22  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog10  hydrog  ?  ?  A  DG   4  N1  ?  ?  ?  1_555  B  DC   9  N3  ?  ?  A  DG   4  B  DC  21  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog11  hydrog  ?  ?  A  DG   4  N2  ?  ?  ?  1_555  B  DC   9  O2  ?  ?  A  DG   4  B  DC  21  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog12  hydrog  ?  ?  A  DG   4  O6  ?  ?  ?  1_555  B  DC   9  N4  ?  ?  A  DG   4  B  DC  21  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog13  hydrog  ?  ?  A  DA   5  N1  ?  ?  ?  1_555  B  DT   8  N3  ?  ?  A  DA   5  B  DT  20  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog14  hydrog  ?  ?  A  DA   5  N6  ?  ?  ?  1_555  B  DT   8  O4  ?  ?  A  DA   5  B  DT  20  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog15  hydrog  ?  ?  A  DA   6  N1  ?  ?  ?  1_555  B  DT   7  N3  ?  ?  A  DA   6  B  DT  19  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog16  hydrog  ?  ?  A  DA   6  N6  ?  ?  ?  1_555  B  DT   7  O4  ?  ?  A  DA   6  B  DT  19  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog17  hydrog  ?  ?  A  DT   7  N3  ?  ?  ?  1_555  B  DA   6  N1  ?  ?  A  DT   7  B  DA  18  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog18  hydrog  ?  ?  A  DT   7  O4  ?  ?  ?  1_555  B  DA   6  N6  ?  ?  A  DT   7  B  DA  18  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog19  hydrog  ?  ?  A  DT   8  N3  ?  ?  ?  1_555  B  DA   5  N1  ?  ?  A  DT   8  B  DA  17  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog20  hydrog  ?  ?  A  DT   8  O4  ?  ?  ?  1_555  B  DA   5  N6  ?  ?  A  DT   8  B  DA  17  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog21  hydrog  ?  ?  A  DC   9  N3  ?  ?  ?  1_555  B  DG   4  N1  ?  ?  A  DC   9  B  DG  16  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog22  hydrog  ?  ?  A  DC   9  N4  ?  ?  ?  1_555  B  DG   4  O6  ?  ?  A  DC   9  B  DG  16  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog23  hydrog  ?  ?  A  DC   9  O2  ?  ?  ?  1_555  B  DG   4  N2  ?  ?  A  DC   9  B  DG  16  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog24  hydrog  ?  ?  A  DG  10  N1  ?  ?  ?  1_555  B  DC   3  N3  ?  ?  A  DG  10  B  DC  15  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog25  hydrog  ?  ?  A  DG  10  N2  ?  ?  ?  1_555  B  DC   3  O2  ?  ?  A  DG  10  B  DC  15  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog26  hydrog  ?  ?  A  DG  10  O6  ?  ?  ?  1_555  B  DC   3  N4  ?  ?  A  DG  10  B  DC  15  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog27  hydrog  ?  ?  A  DC  11  N3  ?  ?  ?  1_555  B  DG   2  N1  ?  ?  A  DC  11  B  DG  14  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog28  hydrog  ?  ?  A  DC  11  N4  ?  ?  ?  1_555  B  DG   2  O6  ?  ?  A  DC  11  B  DG  14  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog29  hydrog  ?  ?  A  DC  11  O2  ?  ?  ?  1_555  B  DG   2  N2  ?  ?  A  DC  11  B  DG  14  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog30  hydrog  ?  ?  A  DG  12  N1  ?  ?  ?  1_555  B  DC   1  N3  ?  ?  A  DG  12  B  DC  13  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog31  hydrog  ?  ?  A  DG  12  N2  ?  ?  ?  1_555  B  DC   1  O2  ?  ?  A  DG  12  B  DC  13  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+hydrog32  hydrog  ?  ?  A  DG  12  O6  ?  ?  ?  1_555  B  DC   1  N4  ?  ?  A  DG  12  B  DC  13  1_555  ?  ?  ?  ?  ?  ?  WATSON-CRICK  ?  ?  
+##
+_struct_conn_type.id         hydrog
+_struct_conn_type.criteria   ?
+_struct_conn_type.reference  ?
+##
+_database_PDB_matrix.entry_id         1BNA
+_database_PDB_matrix.origx[1][1]      1.000000
+_database_PDB_matrix.origx[1][2]      0.000000
+_database_PDB_matrix.origx[1][3]      0.000000
+_database_PDB_matrix.origx[2][1]      0.000000
+_database_PDB_matrix.origx[2][2]      1.000000
+_database_PDB_matrix.origx[2][3]      0.000000
+_database_PDB_matrix.origx[3][1]      0.000000
+_database_PDB_matrix.origx[3][2]      0.000000
+_database_PDB_matrix.origx[3][3]      1.000000
+_database_PDB_matrix.origx_vector[1]  0.00000
+_database_PDB_matrix.origx_vector[2]  0.00000
+_database_PDB_matrix.origx_vector[3]  0.00000
+##
+_atom_sites.entry_id                   1BNA
+_atom_sites.fract_transf_matrix[1][1]  0.040209
+_atom_sites.fract_transf_matrix[1][2]  0.000000
+_atom_sites.fract_transf_matrix[1][3]  0.000000
+_atom_sites.fract_transf_matrix[2][1]  0.000000
+_atom_sites.fract_transf_matrix[2][2]  0.024759
+_atom_sites.fract_transf_matrix[2][3]  0.000000
+_atom_sites.fract_transf_matrix[3][1]  0.000000
+_atom_sites.fract_transf_matrix[3][2]  0.000000
+_atom_sites.fract_transf_matrix[3][3]  0.015106
+_atom_sites.fract_transf_vector[1]     0.00000
+_atom_sites.fract_transf_vector[2]     0.00000
+_atom_sites.fract_transf_vector[3]     0.00000
+##
+loop_
+_atom_type.symbol
+C  
+N  
+O  
+P  
+##
+loop_
+_atom_site.group_PDB
+_atom_site.id
+_atom_site.type_symbol
+_atom_site.label_atom_id
+_atom_site.label_alt_id
+_atom_site.label_comp_id
+_atom_site.label_asym_id
+_atom_site.label_entity_id
+_atom_site.label_seq_id
+_atom_site.pdbx_PDB_ins_code
+_atom_site.Cartn_x
+_atom_site.Cartn_y
+_atom_site.Cartn_z
+_atom_site.occupancy
+_atom_site.B_iso_or_equiv
+_atom_site.pdbx_formal_charge
+_atom_site.auth_seq_id
+_atom_site.auth_comp_id
+_atom_site.auth_asym_id
+_atom_site.auth_atom_id
+_atom_site.pdbx_PDB_model_num
+ATOM      1  O  "O5'"  .  DC   A  1   1  ?  18.935  34.195   25.617  1.00  64.35  ?    1  DC   A  "O5'"  1  
+ATOM      2  C  "C5'"  .  DC   A  1   1  ?  19.130  33.921   24.219  1.00  44.69  ?    1  DC   A  "C5'"  1  
+ATOM      3  C  "C4'"  .  DC   A  1   1  ?  19.961  32.668   24.100  1.00  31.28  ?    1  DC   A  "C4'"  1  
+ATOM      4  O  "O4'"  .  DC   A  1   1  ?  19.360  31.583   24.852  1.00  37.45  ?    1  DC   A  "O4'"  1  
+ATOM      5  C  "C3'"  .  DC   A  1   1  ?  20.172  32.122   22.694  1.00  46.72  ?    1  DC   A  "C3'"  1  
+ATOM      6  O  "O3'"  .  DC   A  1   1  ?  21.350  31.325   22.681  1.00  48.89  ?    1  DC   A  "O3'"  1  
+ATOM      7  C  "C2'"  .  DC   A  1   1  ?  18.948  31.223   22.647  1.00  30.88  ?    1  DC   A  "C2'"  1  
+ATOM      8  C  "C1'"  .  DC   A  1   1  ?  19.231  30.482   23.944  1.00  36.58  ?    1  DC   A  "C1'"  1  
+ATOM      9  N  N1     .  DC   A  1   1  ?  18.070  29.661   24.380  1.00  40.51  ?    1  DC   A  N1     1  
+ATOM     10  C  C2     .  DC   A  1   1  ?  18.224  28.454   25.015  1.00  16.62  ?    1  DC   A  C2     1  
+ATOM     11  O  O2     .  DC   A  1   1  ?  19.360  28.014   25.214  1.00  27.75  ?    1  DC   A  O2     1  
+ATOM     12  N  N3     .  DC   A  1   1  ?  17.143  27.761   25.377  1.00  20.55  ?    1  DC   A  N3     1  
+ATOM     13  C  C4     .  DC   A  1   1  ?  15.917  28.226   25.120  1.00  34.72  ?    1  DC   A  C4     1  
+ATOM     14  N  N4     .  DC   A  1   1  ?  14.828  27.477   25.444  1.00  40.31  ?    1  DC   A  N4     1  
+ATOM     15  C  C5     .  DC   A  1   1  ?  15.719  29.442   24.471  1.00  30.78  ?    1  DC   A  C5     1  
+ATOM     16  C  C6     .  DC   A  1   1  ?  16.843  30.171   24.101  1.00  25.90  ?    1  DC   A  C6     1  
+ATOM     17  P  P      .  DG   A  1   2  ?  22.409  31.286   21.483  1.00  58.85  ?    2  DG   A  P      1  
+ATOM     18  O  OP1    .  DG   A  1   2  ?  23.536  32.157   21.851  1.00  57.82  ?    2  DG   A  OP1    1  
+ATOM     19  O  OP2    .  DG   A  1   2  ?  21.822  31.459   20.139  1.00  78.33  ?    2  DG   A  OP2    1  
+ATOM     20  O  "O5'"  .  DG   A  1   2  ?  22.840  29.751   21.498  1.00  40.36  ?    2  DG   A  "O5'"  1  
+ATOM     21  C  "C5'"  .  DG   A  1   2  ?  23.543  29.175   22.594  1.00  47.19  ?    2  DG   A  "C5'"  1  
+ATOM     22  C  "C4'"  .  DG   A  1   2  ?  23.494  27.709   22.279  1.00  47.81  ?    2  DG   A  "C4'"  1  
+ATOM     23  O  "O4'"  .  DG   A  1   2  ?  22.193  27.252   22.674  1.00  38.76  ?    2  DG   A  "O4'"  1  
+ATOM     24  C  "C3'"  .  DG   A  1   2  ?  23.693  27.325   20.807  1.00  28.58  ?    2  DG   A  "C3'"  1  
+ATOM     25  O  "O3'"  .  DG   A  1   2  ?  24.723  26.320   20.653  1.00  40.44  ?    2  DG   A  "O3'"  1  
+ATOM     26  C  "C2'"  .  DG   A  1   2  ?  22.273  26.885   20.416  1.00  21.14  ?    2  DG   A  "C2'"  1  
+ATOM     27  C  "C1'"  .  DG   A  1   2  ?  21.721  26.304   21.716  1.00  33.95  ?    2  DG   A  "C1'"  1  
+ATOM     28  N  N9     .  DG   A  1   2  ?  20.237  26.470   21.780  1.00  34.00  ?    2  DG   A  N9     1  
+ATOM     29  C  C8     .  DG   A  1   2  ?  19.526  27.584   21.429  1.00  36.47  ?    2  DG   A  C8     1  
+ATOM     30  N  N7     .  DG   A  1   2  ?  18.207  27.455   21.636  1.00  32.37  ?    2  DG   A  N7     1  
+ATOM     31  C  C5     .  DG   A  1   2  ?  18.083  26.212   22.142  1.00  15.06  ?    2  DG   A  C5     1  
+ATOM     32  C  C6     .  DG   A  1   2  ?  16.904  25.525   22.545  1.00  11.88  ?    2  DG   A  C6     1  
+ATOM     33  O  O6     .  DG   A  1   2  ?  15.739  25.916   22.518  1.00  21.30  ?    2  DG   A  O6     1  
+ATOM     34  N  N1     .  DG   A  1   2  ?  17.197  24.279   23.037  1.00  15.44  ?    2  DG   A  N1     1  
+ATOM     35  C  C2     .  DG   A  1   2  ?  18.434  23.717   23.155  1.00   9.63  ?    2  DG   A  C2     1  
+ATOM     36  N  N2     .  DG   A  1   2  ?  18.508  22.456   23.668  1.00  16.69  ?    2  DG   A  N2     1  
+ATOM     37  N  N3     .  DG   A  1   2  ?  19.537  24.360   22.770  1.00  30.98  ?    2  DG   A  N3     1  
+ATOM     38  C  C4     .  DG   A  1   2  ?  19.290  25.594   22.274  1.00  18.56  ?    2  DG   A  C4     1  
+ATOM     39  P  P      .  DC   A  1   3  ?  25.064  25.621   19.252  1.00  44.67  ?    3  DC   A  P      1  
+ATOM     40  O  OP1    .  DC   A  1   3  ?  26.506  25.316   19.220  1.00  53.89  ?    3  DC   A  OP1    1  
+ATOM     41  O  OP2    .  DC   A  1   3  ?  24.559  26.412   18.115  1.00  57.79  ?    3  DC   A  OP2    1  
+ATOM     42  O  "O5'"  .  DC   A  1   3  ?  24.260  24.246   19.327  1.00  35.42  ?    3  DC   A  "O5'"  1  
+ATOM     43  C  "C5'"  .  DC   A  1   3  ?  24.584  23.285   20.335  1.00  45.75  ?    3  DC   A  "C5'"  1  
+ATOM     44  C  "C4'"  .  DC   A  1   3  ?  23.523  22.233   20.245  1.00  43.02  ?    3  DC   A  "C4'"  1  
+ATOM     45  O  "O4'"  .  DC   A  1   3  ?  22.256  22.844   20.453  1.00  36.85  ?    3  DC   A  "O4'"  1  
+ATOM     46  C  "C3'"  .  DC   A  1   3  ?  23.424  21.557   18.903  1.00  40.14  ?    3  DC   A  "C3'"  1  
+ATOM     47  O  "O3'"  .  DC   A  1   3  ?  24.121  20.309   18.928  1.00  49.62  ?    3  DC   A  "O3'"  1  
+ATOM     48  C  "C2'"  .  DC   A  1   3  ?  21.930  21.406   18.661  1.00  53.79  ?    3  DC   A  "C2'"  1  
+ATOM     49  C  "C1'"  .  DC   A  1   3  ?  21.278  21.966   19.909  1.00  22.18  ?    3  DC   A  "C1'"  1  
+ATOM     50  N  N1     .  DC   A  1   3  ?  20.196  22.889   19.521  1.00  25.44  ?    3  DC   A  N1     1  
+ATOM     51  C  C2     .  DC   A  1   3  ?  18.909  22.584   19.816  1.00  19.81  ?    3  DC   A  C2     1  
+ATOM     52  O  O2     .  DC   A  1   3  ?  18.685  21.512   20.382  1.00  29.92  ?    3  DC   A  O2     1  
+ATOM     53  N  N3     .  DC   A  1   3  ?  17.935  23.447   19.502  1.00  21.59  ?    3  DC   A  N3     1  
+ATOM     54  C  C4     .  DC   A  1   3  ?  18.217  24.603   18.897  1.00  14.01  ?    3  DC   A  C4     1  
+ATOM     55  N  N4     .  DC   A  1   3  ?  17.221  25.499   18.629  1.00  26.88  ?    3  DC   A  N4     1  
+ATOM     56  C  C5     .  DC   A  1   3  ?  19.526  24.945   18.571  1.00  27.59  ?    3  DC   A  C5     1  
+ATOM     57  C  C6     .  DC   A  1   3  ?  20.537  24.048   18.899  1.00  27.05  ?    3  DC   A  C6     1  
+ATOM     58  P  P      .  DG   A  1   4  ?  24.249  19.412   17.617  1.00  44.54  ?    4  DG   A  P      1  
+ATOM     59  O  OP1    .  DG   A  1   4  ?  25.420  18.535   17.765  1.00  61.90  ?    4  DG   A  OP1    1  
+ATOM     60  O  OP2    .  DG   A  1   4  ?  24.208  20.296   16.440  1.00  37.36  ?    4  DG   A  OP2    1  
+ATOM     61  O  "O5'"  .  DG   A  1   4  ?  22.931  18.537   17.670  1.00  32.01  ?    4  DG   A  "O5'"  1  
+ATOM     62  C  "C5'"  .  DG   A  1   4  ?  22.714  17.625   18.753  1.00  37.89  ?    4  DG   A  "C5'"  1  
+ATOM     63  C  "C4'"  .  DG   A  1   4  ?  21.393  16.960   18.505  1.00  53.00  ?    4  DG   A  "C4'"  1  
+ATOM     64  O  "O4'"  .  DG   A  1   4  ?  20.353  17.952   18.496  1.00  38.79  ?    4  DG   A  "O4'"  1  
+ATOM     65  C  "C3'"  .  DG   A  1   4  ?  21.264  16.229   17.176  1.00  56.72  ?    4  DG   A  "C3'"  1  
+ATOM     66  O  "O3'"  .  DG   A  1   4  ?  20.284  15.214   17.238  1.00  64.12  ?    4  DG   A  "O3'"  1  
+ATOM     67  C  "C2'"  .  DG   A  1   4  ?  20.793  17.368   16.288  1.00  40.81  ?    4  DG   A  "C2'"  1  
+ATOM     68  C  "C1'"  .  DG   A  1   4  ?  19.716  17.901   17.218  1.00  30.52  ?    4  DG   A  "C1'"  1  
+ATOM     69  N  N9     .  DG   A  1   4  ?  19.305  19.281   16.869  1.00  28.53  ?    4  DG   A  N9     1  
+ATOM     70  C  C8     .  DG   A  1   4  ?  20.017  20.263   16.232  1.00  27.82  ?    4  DG   A  C8     1  
+ATOM     71  N  N7     .  DG   A  1   4  ?  19.313  21.394   16.077  1.00  28.01  ?    4  DG   A  N7     1  
+ATOM     72  C  C5     .  DG   A  1   4  ?  18.121  21.100   16.635  1.00  23.22  ?    4  DG   A  C5     1  
+ATOM     73  C  C6     .  DG   A  1   4  ?  16.952  21.904   16.749  1.00  29.21  ?    4  DG   A  C6     1  
+ATOM     74  O  O6     .  DG   A  1   4  ?  16.769  23.057   16.368  1.00  38.58  ?    4  DG   A  O6     1  
+ATOM     75  N  N1     .  DG   A  1   4  ?  15.933  21.214   17.352  1.00  27.94  ?    4  DG   A  N1     1  
+ATOM     76  C  C2     .  DG   A  1   4  ?  15.972  19.930   17.816  1.00  23.44  ?    4  DG   A  C2     1  
+ATOM     77  N  N2     .  DG   A  1   4  ?  14.831  19.416   18.353  1.00  42.64  ?    4  DG   A  N2     1  
+ATOM     78  N  N3     .  DG   A  1   4  ?  17.068  19.179   17.717  1.00  21.56  ?    4  DG   A  N3     1  
+ATOM     79  C  C4     .  DG   A  1   4  ?  18.084  19.825   17.121  1.00  23.44  ?    4  DG   A  C4     1  
+ATOM     80  P  P      .  DA   A  1   5  ?  20.356  13.969   16.245  1.00  57.01  ?    5  DA   A  P      1  
+ATOM     81  O  OP1    .  DA   A  1   5  ?  21.116  12.891   16.892  1.00  58.59  ?    5  DA   A  OP1    1  
+ATOM     82  O  OP2    .  DA   A  1   5  ?  20.837  14.423   14.910  1.00  51.96  ?    5  DA   A  OP2    1  
+ATOM     83  O  "O5'"  .  DA   A  1   5  ?  18.810  13.581   16.161  1.00  47.12  ?    5  DA   A  "O5'"  1  
+ATOM     84  C  "C5'"  .  DA   A  1   5  ?  18.015  13.569   17.362  1.00  47.67  ?    5  DA   A  "C5'"  1  
+ATOM     85  C  "C4'"  .  DA   A  1   5  ?  16.672  14.088   16.957  1.00  64.79  ?    5  DA   A  "C4'"  1  
+ATOM     86  O  "O4'"  .  DA   A  1   5  ?  16.842  15.447   16.561  1.00  47.60  ?    5  DA   A  "O4'"  1  
+ATOM     87  C  "C3'"  .  DA   A  1   5  ?  16.019  13.393   15.764  1.00  51.50  ?    5  DA   A  "C3'"  1  
+ATOM     88  O  "O3'"  .  DA   A  1   5  ?  14.762  12.796   16.120  1.00  52.18  ?    5  DA   A  "O3'"  1  
+ATOM     89  C  "C2'"  .  DA   A  1   5  ?  15.952  14.498   14.696  1.00  45.00  ?    5  DA   A  "C2'"  1  
+ATOM     90  C  "C1'"  .  DA   A  1   5  ?  15.851  15.732   15.569  1.00  26.88  ?    5  DA   A  "C1'"  1  
+ATOM     91  N  N9     .  DA   A  1   5  ?  16.391  16.916   14.867  1.00  16.69  ?    5  DA   A  N9     1  
+ATOM     92  C  C8     .  DA   A  1   5  ?  17.658  17.103   14.382  1.00  28.14  ?    5  DA   A  C8     1  
+ATOM     93  N  N7     .  DA   A  1   5  ?  17.863  18.346   13.913  1.00  34.85  ?    5  DA   A  N7     1  
+ATOM     94  C  C5     .  DA   A  1   5  ?  16.673  18.953   14.098  1.00  22.49  ?    5  DA   A  C5     1  
+ATOM     95  C  C6     .  DA   A  1   5  ?  16.230  20.279   13.819  1.00  18.12  ?    5  DA   A  C6     1  
+ATOM     96  N  N6     .  DA   A  1   5  ?  17.045  21.222   13.268  1.00  29.30  ?    5  DA   A  N6     1  
+ATOM     97  N  N1     .  DA   A  1   5  ?  14.966  20.578   14.118  1.00  27.61  ?    5  DA   A  N1     1  
+ATOM     98  C  C2     .  DA   A  1   5  ?  14.178  19.652   14.669  1.00  18.53  ?    5  DA   A  C2     1  
+ATOM     99  N  N3     .  DA   A  1   5  ?  14.463  18.392   14.984  1.00  29.16  ?    5  DA   A  N3     1  
+ATOM    100  C  C4     .  DA   A  1   5  ?  15.750  18.110   14.661  1.00  15.08  ?    5  DA   A  C4     1  
+ATOM    101  P  P      .  DA   A  1   6  ?  13.866  12.006   15.063  1.00  43.68  ?    6  DA   A  P      1  
+ATOM    102  O  OP1    .  DA   A  1   6  ?  13.028  11.039   15.800  1.00  42.55  ?    6  DA   A  OP1    1  
+ATOM    103  O  OP2    .  DA   A  1   6  ?  14.715  11.499   13.968  1.00  54.20  ?    6  DA   A  OP2    1  
+ATOM    104  O  "O5'"  .  DA   A  1   6  ?  12.879  13.111   14.480  1.00  28.20  ?    6  DA   A  "O5'"  1  
+ATOM    105  C  "C5'"  .  DA   A  1   6  ?  11.802  13.597   15.290  1.00  42.29  ?    6  DA   A  "C5'"  1  
+ATOM    106  C  "C4'"  .  DA   A  1   6  ?  11.111  14.603   14.435  1.00  33.23  ?    6  DA   A  "C4'"  1  
+ATOM    107  O  "O4'"  .  DA   A  1   6  ?  12.152  15.460   13.962  1.00  41.48  ?    6  DA   A  "O4'"  1  
+ATOM    108  C  "C3'"  .  DA   A  1   6  ?  10.417  14.070   13.187  1.00  18.16  ?    6  DA   A  "C3'"  1  
+ATOM    109  O  "O3'"  .  DA   A  1   6  ?   9.007  14.369   13.181  1.00  30.42  ?    6  DA   A  "O3'"  1  
+ATOM    110  C  "C2'"  .  DA   A  1   6  ?  11.240  14.692   12.061  1.00  52.97  ?    6  DA   A  "C2'"  1  
+ATOM    111  C  "C1'"  .  DA   A  1   6  ?  11.699  15.974   12.719  1.00  38.93  ?    6  DA   A  "C1'"  1  
+ATOM    112  N  N9     .  DA   A  1   6  ?  12.918  16.526   12.078  1.00  19.06  ?    6  DA   A  N9     1  
+ATOM    113  C  C8     .  DA   A  1   6  ?  14.115  15.899   11.868  1.00  17.83  ?    6  DA   A  C8     1  
+ATOM    114  N  N7     .  DA   A  1   6  ?  15.049  16.714   11.356  1.00  29.55  ?    6  DA   A  N7     1  
+ATOM    115  C  C5     .  DA   A  1   6  ?  14.416  17.901   11.246  1.00  19.88  ?    6  DA   A  C5     1  
+ATOM    116  C  C6     .  DA   A  1   6  ?  14.873  19.187   10.815  1.00  17.26  ?    6  DA   A  C6     1  
+ATOM    117  N  N6     .  DA   A  1   6  ?  16.161  19.418   10.427  1.00  19.85  ?    6  DA   A  N6     1  
+ATOM    118  N  N1     .  DA   A  1   6  ?  13.999  20.191   10.852  1.00  17.93  ?    6  DA   A  N1     1  
+ATOM    119  C  C2     .  DA   A  1   6  ?  12.753  19.962   11.272  1.00  23.00  ?    6  DA   A  C2     1  
+ATOM    120  N  N3     .  DA   A  1   6  ?  12.210  18.824   11.698  1.00  21.37  ?    6  DA   A  N3     1  
+ATOM    121  C  C4     .  DA   A  1   6  ?  13.116  17.823   11.657  1.00  15.93  ?    6  DA   A  C4     1  
+ATOM    122  P  P      .  DT   A  1   7  ?   8.081  14.050   11.915  1.00  40.72  ?    7  DT   A  P      1  
+ATOM    123  O  OP1    .  DT   A  1   7  ?   6.668  13.960   12.342  1.00  46.75  ?    7  DT   A  OP1    1  
+ATOM    124  O  OP2    .  DT   A  1   7  ?   8.600  12.894   11.137  1.00  42.53  ?    7  DT   A  OP2    1  
+ATOM    125  O  "O5'"  .  DT   A  1   7  ?   8.239  15.387   11.076  1.00  35.21  ?    7  DT   A  "O5'"  1  
+ATOM    126  C  "C5'"  .  DT   A  1   7  ?   7.907  16.635   11.686  1.00  34.88  ?    7  DT   A  "C5'"  1  
+ATOM    127  C  "C4'"  .  DT   A  1   7  ?   8.162  17.628   10.598  1.00  31.45  ?    7  DT   A  "C4'"  1  
+ATOM    128  O  "O4'"  .  DT   A  1   7  ?   9.543  17.580   10.279  1.00  46.82  ?    7  DT   A  "O4'"  1  
+ATOM    129  C  "C3'"  .  DT   A  1   7  ?   7.461  17.284    9.296  1.00  23.76  ?    7  DT   A  "C3'"  1  
+ATOM    130  O  "O3'"  .  DT   A  1   7  ?   6.251  18.034    9.162  1.00  44.27  ?    7  DT   A  "O3'"  1  
+ATOM    131  C  "C2'"  .  DT   A  1   7  ?   8.532  17.527    8.223  1.00  26.30  ?    7  DT   A  "C2'"  1  
+ATOM    132  C  "C1'"  .  DT   A  1   7  ?   9.644  18.209    9.019  1.00  28.96  ?    7  DT   A  "C1'"  1  
+ATOM    133  N  N1     .  DT   A  1   7  ?  11.021  17.903    8.565  1.00  20.47  ?    7  DT   A  N1     1  
+ATOM    134  C  C2     .  DT   A  1   7  ?  11.822  18.923    8.176  1.00  28.01  ?    7  DT   A  C2     1  
+ATOM    135  O  O2     .  DT   A  1   7  ?  11.383  20.077    8.143  1.00  40.01  ?    7  DT   A  O2     1  
+ATOM    136  N  N3     .  DT   A  1   7  ?  13.119  18.641    7.852  1.00  27.94  ?    7  DT   A  N3     1  
+ATOM    137  C  C4     .  DT   A  1   7  ?  13.633  17.372    7.882  1.00  15.14  ?    7  DT   A  C4     1  
+ATOM    138  O  O4     .  DT   A  1   7  ?  14.830  17.222    7.619  1.00  32.54  ?    7  DT   A  O4     1  
+ATOM    139  C  C5     .  DT   A  1   7  ?  12.781  16.325    8.235  1.00  10.83  ?    7  DT   A  C5     1  
+ATOM    140  C  C7     .  DT   A  1   7  ?  13.269  14.902    8.236  1.00  36.33  ?    7  DT   A  C7     1  
+ATOM    141  C  C6     .  DT   A  1   7  ?  11.465  16.616    8.594  1.00  12.19  ?    7  DT   A  C6     1  
+ATOM    142  P  P      .  DT   A  1   8  ?   5.384  17.990    7.824  1.00  49.10  ?    8  DT   A  P      1  
+ATOM    143  O  OP1    .  DT   A  1   8  ?   4.025  18.444    8.180  1.00  41.11  ?    8  DT   A  OP1    1  
+ATOM    144  O  OP2    .  DT   A  1   8  ?   5.458  16.668    7.160  1.00  39.21  ?    8  DT   A  OP2    1  
+ATOM    145  O  "O5'"  .  DT   A  1   8  ?   6.086  19.118    6.927  1.00  48.80  ?    8  DT   A  "O5'"  1  
+ATOM    146  C  "C5'"  .  DT   A  1   8  ?   6.146  20.478    7.418  1.00  34.73  ?    8  DT   A  "C5'"  1  
+ATOM    147  C  "C4'"  .  DT   A  1   8  ?   6.995  21.229    6.438  1.00  28.73  ?    8  DT   A  "C4'"  1  
+ATOM    148  O  "O4'"  .  DT   A  1   8  ?   8.188  20.458    6.284  1.00  39.07  ?    8  DT   A  "O4'"  1  
+ATOM    149  C  "C3'"  .  DT   A  1   8  ?   6.418  21.332    5.029  1.00  37.88  ?    8  DT   A  "C3'"  1  
+ATOM    150  O  "O3'"  .  DT   A  1   8  ?   5.967  22.667    4.696  1.00  52.04  ?    8  DT   A  "O3'"  1  
+ATOM    151  C  "C2'"  .  DT   A  1   8  ?   7.513  20.718    4.139  1.00  32.80  ?    8  DT   A  "C2'"  1  
+ATOM    152  C  "C1'"  .  DT   A  1   8  ?   8.736  20.855    5.034  1.00  36.58  ?    8  DT   A  "C1'"  1  
+ATOM    153  N  N1     .  DT   A  1   8  ?   9.823  19.876    4.759  1.00  24.57  ?    8  DT   A  N1     1  
+ATOM    154  C  C2     .  DT   A  1   8  ?  11.086  20.316    4.494  1.00  19.41  ?    8  DT   A  C2     1  
+ATOM    155  O  O2     .  DT   A  1   8  ?  11.324  21.516    4.389  1.00  32.74  ?    8  DT   A  O2     1  
+ATOM    156  N  N3     .  DT   A  1   8  ?  12.094  19.403    4.412  1.00  25.12  ?    8  DT   A  N3     1  
+ATOM    157  C  C4     .  DT   A  1   8  ?  11.876  18.060    4.551  1.00  31.35  ?    8  DT   A  C4     1  
+ATOM    158  O  O4     .  DT   A  1   8  ?  12.858  17.317    4.503  1.00  28.53  ?    8  DT   A  O4     1  
+ATOM    159  C  C5     .  DT   A  1   8  ?  10.569  17.611    4.765  1.00  22.80  ?    8  DT   A  C5     1  
+ATOM    160  C  C7     .  DT   A  1   8  ?  10.261  16.140    4.896  1.00  24.98  ?    8  DT   A  C7     1  
+ATOM    161  C  C6     .  DT   A  1   8  ?   9.545  18.548    4.904  1.00  20.28  ?    8  DT   A  C6     1  
+ATOM    162  P  P      .  DC   A  1   9  ?   5.531  23.071    3.209  1.00  48.97  ?    9  DC   A  P      1  
+ATOM    163  O  OP1    .  DC   A  1   9  ?   4.648  24.244    3.269  1.00  62.33  ?    9  DC   A  OP1    1  
+ATOM    164  O  OP2    .  DC   A  1   9  ?   5.010  21.905    2.470  1.00  51.53  ?    9  DC   A  OP2    1  
+ATOM    165  O  "O5'"  .  DC   A  1   9  ?   6.926  23.547    2.611  1.00  43.99  ?    9  DC   A  "O5'"  1  
+ATOM    166  C  "C5'"  .  DC   A  1   9  ?   7.636  24.627    3.249  1.00  50.86  ?    9  DC   A  "C5'"  1  
+ATOM    167  C  "C4'"  .  DC   A  1   9  ?   8.897  24.853    2.457  1.00  46.66  ?    9  DC   A  "C4'"  1  
+ATOM    168  O  "O4'"  .  DC   A  1   9  ?   9.638  23.627    2.448  1.00  42.69  ?    9  DC   A  "O4'"  1  
+ATOM    169  C  "C3'"  .  DC   A  1   9  ?   8.717  25.240    0.998  1.00  56.96  ?    9  DC   A  "C3'"  1  
+ATOM    170  O  "O3'"  .  DC   A  1   9  ?   9.470  26.414    0.667  1.00  63.54  ?    9  DC   A  "O3'"  1  
+ATOM    171  C  "C2'"  .  DC   A  1   9  ?   9.126  23.965    0.253  1.00  50.41  ?    9  DC   A  "C2'"  1  
+ATOM    172  C  "C1'"  .  DC   A  1   9  ?  10.241  23.483    1.157  1.00  41.08  ?    9  DC   A  "C1'"  1  
+ATOM    173  N  N1     .  DC   A  1   9  ?  10.524  22.022    1.015  1.00  37.23  ?    9  DC   A  N1     1  
+ATOM    174  C  C2     .  DC   A  1   9  ?  11.814  21.603    0.840  1.00  40.54  ?    9  DC   A  C2     1  
+ATOM    175  O  O2     .  DC   A  1   9  ?  12.691  22.447    0.670  1.00  43.89  ?    9  DC   A  O2     1  
+ATOM    176  N  N3     .  DC   A  1   9  ?  12.106  20.297    0.873  1.00  32.57  ?    9  DC   A  N3     1  
+ATOM    177  C  C4     .  DC   A  1   9  ?  11.141  19.395    1.046  1.00  24.65  ?    9  DC   A  C4     1  
+ATOM    178  N  N4     .  DC   A  1   9  ?  11.461  18.075    1.089  1.00  27.84  ?    9  DC   A  N4     1  
+ATOM    179  C  C5     .  DC   A  1   9  ?   9.803  19.775    1.177  1.00  17.61  ?    9  DC   A  C5     1  
+ATOM    180  C  C6     .  DC   A  1   9  ?   9.499  21.133    1.167  1.00  30.63  ?    9  DC   A  C6     1  
+ATOM    181  P  P      .  DG   A  1  10  ?   9.055  27.333   -0.581  1.00  65.48  ?   10  DG   A  P      1  
+ATOM    182  O  OP1    .  DG   A  1  10  ?   9.496  28.717   -0.258  1.00  59.09  ?   10  DG   A  OP1    1  
+ATOM    183  O  OP2    .  DG   A  1  10  ?   7.632  27.106   -0.947  1.00  45.71  ?   10  DG   A  OP2    1  
+ATOM    184  O  "O5'"  .  DG   A  1  10  ?   9.954  26.765   -1.771  1.00  70.30  ?   10  DG   A  "O5'"  1  
+ATOM    185  C  "C5'"  .  DG   A  1  10  ?  11.382  26.940   -1.720  1.00  71.73  ?   10  DG   A  "C5'"  1  
+ATOM    186  C  "C4'"  .  DG   A  1  10  ?  11.972  26.090   -2.802  1.00  58.69  ?   10  DG   A  "C4'"  1  
+ATOM    187  O  "O4'"  .  DG   A  1  10  ?  11.802  24.724   -2.404  1.00  41.03  ?   10  DG   A  "O4'"  1  
+ATOM    188  C  "C3'"  .  DG   A  1  10  ?  11.327  26.178   -4.188  1.00  45.61  ?   10  DG   A  "C3'"  1  
+ATOM    189  O  "O3'"  .  DG   A  1  10  ?  12.311  26.096   -5.214  1.00  52.70  ?   10  DG   A  "O3'"  1  
+ATOM    190  C  "C2'"  .  DG   A  1  10  ?  10.414  24.962   -4.186  1.00  36.02  ?   10  DG   A  "C2'"  1  
+ATOM    191  C  "C1'"  .  DG   A  1  10  ?  11.429  24.028   -3.587  1.00  50.90  ?   10  DG   A  "C1'"  1  
+ATOM    192  N  N9     .  DG   A  1  10  ?  10.890  22.713   -3.200  1.00  45.86  ?   10  DG   A  N9     1  
+ATOM    193  C  C8     .  DG   A  1  10  ?   9.616  22.315   -2.910  1.00  44.49  ?   10  DG   A  C8     1  
+ATOM    194  N  N7     .  DG   A  1  10  ?   9.541  21.009   -2.613  1.00  39.96  ?   10  DG   A  N7     1  
+ATOM    195  C  C5     .  DG   A  1  10  ?  10.818  20.588   -2.718  1.00  38.99  ?   10  DG   A  C5     1  
+ATOM    196  C  C6     .  DG   A  1  10  ?  11.376  19.292   -2.511  1.00  35.78  ?   10  DG   A  C6     1  
+ATOM    197  O  O6     .  DG   A  1  10  ?  10.813  18.252   -2.179  1.00  34.90  ?   10  DG   A  O6     1  
+ATOM    198  N  N1     .  DG   A  1  10  ?  12.729  19.299   -2.720  1.00  23.54  ?   10  DG   A  N1     1  
+ATOM    199  C  C2     .  DG   A  1  10  ?  13.498  20.365   -3.082  1.00   8.73  ?   10  DG   A  C2     1  
+ATOM    200  N  N2     .  DG   A  1  10  ?  14.834  20.169   -3.237  1.00  23.15  ?   10  DG   A  N2     1  
+ATOM    201  N  N3     .  DG   A  1  10  ?  12.982  21.573   -3.267  1.00  24.68  ?   10  DG   A  N3     1  
+ATOM    202  C  C4     .  DG   A  1  10  ?  11.656  21.601   -3.061  1.00  31.53  ?   10  DG   A  C4     1  
+ATOM    203  P  P      .  DC   A  1  11  ?  12.763  27.421   -5.980  1.00  60.62  ?   11  DC   A  P      1  
+ATOM    204  O  OP1    .  DC   A  1  11  ?  12.796  28.572   -5.049  1.00  63.74  ?   11  DC   A  OP1    1  
+ATOM    205  O  OP2    .  DC   A  1  11  ?  11.886  27.542   -7.164  1.00  52.44  ?   11  DC   A  OP2    1  
+ATOM    206  O  "O5'"  .  DC   A  1  11  ?  14.272  27.086   -6.366  1.00  57.57  ?   11  DC   A  "O5'"  1  
+ATOM    207  C  "C5'"  .  DC   A  1  11  ?  15.275  27.108   -5.318  1.00  54.70  ?   11  DC   A  "C5'"  1  
+ATOM    208  C  "C4'"  .  DC   A  1  11  ?  16.222  25.946   -5.510  1.00  72.51  ?   11  DC   A  "C4'"  1  
+ATOM    209  O  "O4'"  .  DC   A  1  11  ?  15.443  24.754   -5.397  1.00  47.18  ?   11  DC   A  "O4'"  1  
+ATOM    210  C  "C3'"  .  DC   A  1  11  ?  16.942  25.827   -6.848  1.00  29.82  ?   11  DC   A  "C3'"  1  
+ATOM    211  O  "O3'"  .  DC   A  1  11  ?  18.340  25.511   -6.701  1.00  43.53  ?   11  DC   A  "O3'"  1  
+ATOM    212  C  "C2'"  .  DC   A  1  11  ?  16.118  24.767   -7.578  1.00  51.34  ?   11  DC   A  "C2'"  1  
+ATOM    213  C  "C1'"  .  DC   A  1  11  ?  15.856  23.836   -6.414  1.00  30.07  ?   11  DC   A  "C1'"  1  
+ATOM    214  N  N1     .  DC   A  1  11  ?  14.672  22.975   -6.637  1.00  23.25  ?   11  DC   A  N1     1  
+ATOM    215  C  C2     .  DC   A  1  11  ?  14.802  21.628   -6.529  1.00  20.38  ?   11  DC   A  C2     1  
+ATOM    216  O  O2     .  DC   A  1  11  ?  15.924  21.178   -6.314  1.00  38.77  ?   11  DC   A  O2     1  
+ATOM    217  N  N3     .  DC   A  1  11  ?  13.723  20.842   -6.627  1.00  15.92  ?   11  DC   A  N3     1  
+ATOM    218  C  C4     .  DC   A  1  11  ?  12.515  21.373   -6.836  1.00  15.82  ?   11  DC   A  C4     1  
+ATOM    219  N  N4     .  DC   A  1  11  ?  11.410  20.574   -6.872  1.00  28.04  ?   11  DC   A  N4     1  
+ATOM    220  C  C5     .  DC   A  1  11  ?  12.348  22.744   -6.978  1.00  26.17  ?   11  DC   A  C5     1  
+ATOM    221  C  C6     .  DC   A  1  11  ?  13.470  23.558   -6.869  1.00  35.50  ?   11  DC   A  C6     1  
+ATOM    222  P  P      .  DG   A  1  12  ?  19.331  25.774   -7.925  1.00  55.98  ?   12  DG   A  P      1  
+ATOM    223  O  OP1    .  DG   A  1  12  ?  20.704  25.976   -7.408  1.00  45.83  ?   12  DG   A  OP1    1  
+ATOM    224  O  OP2    .  DG   A  1  12  ?  18.763  26.851   -8.758  1.00  44.26  ?   12  DG   A  OP2    1  
+ATOM    225  O  "O5'"  .  DG   A  1  12  ?  19.302  24.412   -8.763  1.00  62.63  ?   12  DG   A  "O5'"  1  
+ATOM    226  C  "C5'"  .  DG   A  1  12  ?  20.109  23.284   -8.359  1.00  69.50  ?   12  DG   A  "C5'"  1  
+ATOM    227  C  "C4'"  .  DG   A  1  12  ?  19.748  22.167   -9.299  1.00  39.92  ?   12  DG   A  "C4'"  1  
+ATOM    228  O  "O4'"  .  DG   A  1  12  ?  18.350  21.969   -9.139  1.00  32.00  ?   12  DG   A  "O4'"  1  
+ATOM    229  C  "C3'"  .  DG   A  1  12  ?  19.921  22.404  -10.815  1.00  50.39  ?   12  DG   A  "C3'"  1  
+ATOM    230  O  "O3'"  .  DG   A  1  12  ?  20.985  21.635  -11.401  1.00  64.13  ?   12  DG   A  "O3'"  1  
+ATOM    231  C  "C2'"  .  DG   A  1  12  ?  18.535  22.062  -11.381  1.00  36.18  ?   12  DG   A  "C2'"  1  
+ATOM    232  C  "C1'"  .  DG   A  1  12  ?  17.965  21.200  -10.269  1.00  24.79  ?   12  DG   A  "C1'"  1  
+ATOM    233  N  N9     .  DG   A  1  12  ?  16.493  21.220  -10.265  1.00  28.44  ?   12  DG   A  N9     1  
+ATOM    234  C  C8     .  DG   A  1  12  ?  15.663  22.289  -10.478  1.00  31.85  ?   12  DG   A  C8     1  
+ATOM    235  N  N7     .  DG   A  1  12  ?  14.368  21.958  -10.390  1.00  38.26  ?   12  DG   A  N7     1  
+ATOM    236  C  C5     .  DG   A  1  12  ?  14.388  20.640  -10.102  1.00  28.99  ?   12  DG   A  C5     1  
+ATOM    237  C  C6     .  DG   A  1  12  ?  13.301  19.742   -9.856  1.00  42.63  ?   12  DG   A  C6     1  
+ATOM    238  O  O6     .  DG   A  1  12  ?  12.091  19.967   -9.857  1.00  49.17  ?   12  DG   A  O6     1  
+ATOM    239  N  N1     .  DG   A  1  12  ?  13.750  18.466   -9.625  1.00  40.15  ?   12  DG   A  N1     1  
+ATOM    240  C  C2     .  DG   A  1  12  ?  15.042  18.043   -9.605  1.00  33.42  ?   12  DG   A  C2     1  
+ATOM    241  N  N2     .  DG   A  1  12  ?  15.259  16.717   -9.406  1.00  40.53  ?   12  DG   A  N2     1  
+ATOM    242  N  N3     .  DG   A  1  12  ?  16.061  18.885   -9.792  1.00  37.34  ?   12  DG   A  N3     1  
+ATOM    243  C  C4     .  DG   A  1  12  ?  15.660  20.156  -10.027  1.00  31.14  ?   12  DG   A  C4     1  
+ATOM    244  O  "O5'"  .  DC   B  1   1  ?   7.458  11.884   -9.070  1.00  66.23  ?   13  DC   B  "O5'"  1  
+ATOM    245  C  "C5'"  .  DC   B  1   1  ?   8.252  10.968   -9.854  1.00  71.49  ?   13  DC   B  "C5'"  1  
+ATOM    246  C  "C4'"  .  DC   B  1   1  ?   9.714  11.141   -9.512  1.00  56.82  ?   13  DC   B  "C4'"  1  
+ATOM    247  O  "O4'"  .  DC   B  1   1  ?  10.144  12.455   -9.908  1.00  57.92  ?   13  DC   B  "O4'"  1  
+ATOM    248  C  "C3'"  .  DC   B  1   1  ?  10.103  10.989   -8.055  1.00  34.34  ?   13  DC   B  "C3'"  1  
+ATOM    249  O  "O3'"  .  DC   B  1   1  ?  11.293  10.221   -7.904  1.00  42.11  ?   13  DC   B  "O3'"  1  
+ATOM    250  C  "C2'"  .  DC   B  1   1  ?  10.254  12.437   -7.607  1.00  29.08  ?   13  DC   B  "C2'"  1  
+ATOM    251  C  "C1'"  .  DC   B  1   1  ?  10.896  13.044   -8.837  1.00  38.40  ?   13  DC   B  "C1'"  1  
+ATOM    252  N  N1     .  DC   B  1   1  ?  10.575  14.487   -8.944  1.00  34.33  ?   13  DC   B  N1     1  
+ATOM    253  C  C2     .  DC   B  1   1  ?  11.559  15.430   -9.006  1.00  22.98  ?   13  DC   B  C2     1  
+ATOM    254  O  O2     .  DC   B  1   1  ?  12.725  15.066   -8.932  1.00  50.83  ?   13  DC   B  O2     1  
+ATOM    255  N  N3     .  DC   B  1   1  ?  11.246  16.714   -9.193  1.00  37.14  ?   13  DC   B  N3     1  
+ATOM    256  C  C4     .  DC   B  1   1  ?   9.980  17.088   -9.334  1.00  42.60  ?   13  DC   B  C4     1  
+ATOM    257  N  N4     .  DC   B  1   1  ?   9.698  18.395   -9.589  1.00  54.91  ?   13  DC   B  N4     1  
+ATOM    258  C  C5     .  DC   B  1   1  ?   8.939  16.162   -9.274  1.00  56.67  ?   13  DC   B  C5     1  
+ATOM    259  C  C6     .  DC   B  1   1  ?   9.265  14.824   -9.080  1.00  49.21  ?   13  DC   B  C6     1  
+ATOM    260  P  P      .  DG   B  1   2  ?  11.602   9.510   -6.502  1.00  60.42  ?   14  DG   B  P      1  
+ATOM    261  O  OP1    .  DG   B  1   2  ?  11.666   8.032   -6.664  1.00  57.44  ?   14  DG   B  OP1    1  
+ATOM    262  O  OP2    .  DG   B  1   2  ?  10.644  10.010   -5.494  1.00  46.07  ?   14  DG   B  OP2    1  
+ATOM    263  O  "O5'"  .  DG   B  1   2  ?  13.051  10.094   -6.177  1.00  50.94  ?   14  DG   B  "O5'"  1  
+ATOM    264  C  "C5'"  .  DG   B  1   2  ?  14.100  10.021   -7.156  1.00  34.84  ?   14  DG   B  "C5'"  1  
+ATOM    265  C  "C4'"  .  DG   B  1   2  ?  15.113  10.992   -6.657  1.00  48.06  ?   14  DG   B  "C4'"  1  
+ATOM    266  O  "O4'"  .  DG   B  1   2  ?  14.556  12.300   -6.755  1.00  37.01  ?   14  DG   B  "O4'"  1  
+ATOM    267  C  "C3'"  .  DG   B  1   2  ?  15.445  10.806   -5.189  1.00  50.58  ?   14  DG   B  "C3'"  1  
+ATOM    268  O  "O3'"  .  DG   B  1   2  ?  16.836  10.560   -5.013  1.00  51.98  ?   14  DG   B  "O3'"  1  
+ATOM    269  C  "C2'"  .  DG   B  1   2  ?  14.937  12.100   -4.529  1.00  40.32  ?   14  DG   B  "C2'"  1  
+ATOM    270  C  "C1'"  .  DG   B  1   2  ?  15.058  13.086   -5.671  1.00  46.69  ?   14  DG   B  "C1'"  1  
+ATOM    271  N  N9     .  DG   B  1   2  ?  14.036  14.140   -5.536  1.00  29.17  ?   14  DG   B  N9     1  
+ATOM    272  C  C8     .  DG   B  1   2  ?  12.710  13.957   -5.259  1.00  23.48  ?   14  DG   B  C8     1  
+ATOM    273  N  N7     .  DG   B  1   2  ?  12.016  15.103   -5.269  1.00  37.54  ?   14  DG   B  N7     1  
+ATOM    274  C  C5     .  DG   B  1   2  ?  12.937  16.041   -5.558  1.00  26.27  ?   14  DG   B  C5     1  
+ATOM    275  C  C6     .  DG   B  1   2  ?  12.761  17.451   -5.710  1.00  40.82  ?   14  DG   B  C6     1  
+ATOM    276  O  O6     .  DG   B  1   2  ?  11.723  18.111   -5.630  1.00  44.39  ?   14  DG   B  O6     1  
+ATOM    277  N  N1     .  DG   B  1   2  ?  13.952  18.079   -5.973  1.00  19.52  ?   14  DG   B  N1     1  
+ATOM    278  C  C2     .  DG   B  1   2  ?  15.171  17.485   -6.107  1.00  18.48  ?   14  DG   B  C2     1  
+ATOM    279  N  N2     .  DG   B  1   2  ?  16.244  18.292   -6.325  1.00  36.58  ?   14  DG   B  N2     1  
+ATOM    280  N  N3     .  DG   B  1   2  ?  15.329  16.161   -5.986  1.00  46.96  ?   14  DG   B  N3     1  
+ATOM    281  C  C4     .  DG   B  1   2  ?  14.179  15.499   -5.721  1.00  35.70  ?   14  DG   B  C4     1  
+ATOM    282  P  P      .  DC   B  1   3  ?  17.478  10.380   -3.569  1.00  46.26  ?   15  DC   B  P      1  
+ATOM    283  O  OP1    .  DC   B  1   3  ?  18.665   9.516   -3.729  1.00  46.07  ?   15  DC   B  OP1    1  
+ATOM    284  O  OP2    .  DC   B  1   3  ?  16.427   9.940   -2.633  1.00  40.43  ?   15  DC   B  OP2    1  
+ATOM    285  O  "O5'"  .  DC   B  1   3  ?  17.957  11.865   -3.208  1.00  40.97  ?   15  DC   B  "O5'"  1  
+ATOM    286  C  "C5'"  .  DC   B  1   3  ?  18.963  12.531   -3.996  1.00  28.78  ?   15  DC   B  "C5'"  1  
+ATOM    287  C  "C4'"  .  DC   B  1   3  ?  18.936  13.958   -3.536  1.00  32.84  ?   15  DC   B  "C4'"  1  
+ATOM    288  O  "O4'"  .  DC   B  1   3  ?  17.592  14.409   -3.622  1.00  37.24  ?   15  DC   B  "O4'"  1  
+ATOM    289  C  "C3'"  .  DC   B  1   3  ?  19.253  14.139   -2.066  1.00  43.98  ?   15  DC   B  "C3'"  1  
+ATOM    290  O  "O3'"  .  DC   B  1   3  ?  20.659  14.219   -1.858  1.00  40.90  ?   15  DC   B  "O3'"  1  
+ATOM    291  C  "C2'"  .  DC   B  1   3  ?  18.520  15.417   -1.728  1.00  36.26  ?   15  DC   B  "C2'"  1  
+ATOM    292  C  "C1'"  .  DC   B  1   3  ?  17.545  15.602   -2.872  1.00  20.54  ?   15  DC   B  "C1'"  1  
+ATOM    293  N  N1     .  DC   B  1   3  ?  16.145  15.696   -2.428  1.00  23.10  ?   15  DC   B  N1     1  
+ATOM    294  C  C2     .  DC   B  1   3  ?  15.507  16.886   -2.558  1.00  32.12  ?   15  DC   B  C2     1  
+ATOM    295  O  O2     .  DC   B  1   3  ?  16.162  17.846   -2.957  1.00  30.04  ?   15  DC   B  O2     1  
+ATOM    296  N  N3     .  DC   B  1   3  ?  14.209  16.983   -2.264  1.00  32.94  ?   15  DC   B  N3     1  
+ATOM    297  C  C4     .  DC   B  1   3  ?  13.536  15.919   -1.825  1.00  16.43  ?   15  DC   B  C4     1  
+ATOM    298  N  N4     .  DC   B  1   3  ?  12.205  16.017   -1.553  1.00  34.91  ?   15  DC   B  N4     1  
+ATOM    299  C  C5     .  DC   B  1   3  ?  14.164  14.689   -1.652  1.00  22.75  ?   15  DC   B  C5     1  
+ATOM    300  C  C6     .  DC   B  1   3  ?  15.509  14.584   -1.979  1.00  26.42  ?   15  DC   B  C6     1  
+ATOM    301  P  P      .  DG   B  1   4  ?  21.304  14.529   -0.436  1.00  42.39  ?   16  DG   B  P      1  
+ATOM    302  O  OP1    .  DG   B  1   4  ?  22.696  14.087   -0.524  1.00  60.41  ?   16  DG   B  OP1    1  
+ATOM    303  O  OP2    .  DG   B  1   4  ?  20.488  13.954    0.650  1.00  51.09  ?   16  DG   B  OP2    1  
+ATOM    304  O  "O5'"  .  DG   B  1   4  ?  21.306  16.117   -0.363  1.00  45.08  ?   16  DG   B  "O5'"  1  
+ATOM    305  C  "C5'"  .  DG   B  1   4  ?  22.177  16.876   -1.212  1.00  33.20  ?   16  DG   B  "C5'"  1  
+ATOM    306  C  "C4'"  .  DG   B  1   4  ?  21.739  18.292   -1.021  1.00  24.95  ?   16  DG   B  "C4'"  1  
+ATOM    307  O  "O4'"  .  DG   B  1   4  ?  20.305  18.225   -1.048  1.00  32.83  ?   16  DG   B  "O4'"  1  
+ATOM    308  C  "C3'"  .  DG   B  1   4  ?  22.101  18.959    0.293  1.00  41.12  ?   16  DG   B  "C3'"  1  
+ATOM    309  O  "O3'"  .  DG   B  1   4  ?  22.592  20.293    0.097  1.00  53.45  ?   16  DG   B  "O3'"  1  
+ATOM    310  C  "C2'"  .  DG   B  1   4  ?  20.820  18.829    1.121  1.00  28.93  ?   16  DG   B  "C2'"  1  
+ATOM    311  C  "C1'"  .  DG   B  1   4  ?  19.765  18.985    0.046  1.00  37.44  ?   16  DG   B  "C1'"  1  
+ATOM    312  N  N9     .  DG   B  1   4  ?  18.513  18.299    0.468  1.00  17.75  ?   16  DG   B  N9     1  
+ATOM    313  C  C8     .  DG   B  1   4  ?  18.363  17.062    1.039  1.00  17.96  ?   16  DG   B  C8     1  
+ATOM    314  N  N7     .  DG   B  1   4  ?  17.080  16.744    1.281  1.00  24.14  ?   16  DG   B  N7     1  
+ATOM    315  C  C5     .  DG   B  1   4  ?  16.400  17.832    0.868  1.00   9.96  ?   16  DG   B  C5     1  
+ATOM    316  C  C6     .  DG   B  1   4  ?  14.996  18.090    0.882  1.00  18.10  ?   16  DG   B  C6     1  
+ATOM    317  O  O6     .  DG   B  1   4  ?  14.082  17.378    1.280  1.00  31.13  ?   16  DG   B  O6     1  
+ATOM    318  N  N1     .  DG   B  1   4  ?  14.712  19.349    0.418  1.00  17.72  ?   16  DG   B  N1     1  
+ATOM    319  C  C2     .  DG   B  1   4  ?  15.606  20.268   -0.027  1.00  16.23  ?   16  DG   B  C2     1  
+ATOM    320  N  N2     .  DG   B  1   4  ?  15.134  21.493   -0.382  1.00  33.42  ?   16  DG   B  N2     1  
+ATOM    321  N  N3     .  DG   B  1   4  ?  16.912  20.017   -0.072  1.00  26.37  ?   16  DG   B  N3     1  
+ATOM    322  C  C4     .  DG   B  1   4  ?  17.236  18.794    0.384  1.00  31.72  ?   16  DG   B  C4     1  
+ATOM    323  P  P      .  DA   B  1   5  ?  22.904  21.238    1.339  1.00  46.87  ?   17  DA   B  P      1  
+ATOM    324  O  OP1    .  DA   B  1   5  ?  23.994  22.183    1.025  1.00  47.75  ?   17  DA   B  OP1    1  
+ATOM    325  O  OP2    .  DA   B  1   5  ?  23.104  20.390    2.538  1.00  46.81  ?   17  DA   B  OP2    1  
+ATOM    326  O  "O5'"  .  DA   B  1   5  ?  21.577  22.107    1.390  1.00  39.51  ?   17  DA   B  "O5'"  1  
+ATOM    327  C  "C5'"  .  DA   B  1   5  ?  21.216  22.833    0.200  1.00  30.37  ?   17  DA   B  "C5'"  1  
+ATOM    328  C  "C4'"  .  DA   B  1   5  ?  20.101  23.788    0.484  1.00  35.43  ?   17  DA   B  "C4'"  1  
+ATOM    329  O  "O4'"  .  DA   B  1   5  ?  18.913  23.054    0.816  1.00  43.05  ?   17  DA   B  "O4'"  1  
+ATOM    330  C  "C3'"  .  DA   B  1   5  ?  20.347  24.743    1.633  1.00  44.50  ?   17  DA   B  "C3'"  1  
+ATOM    331  O  "O3'"  .  DA   B  1   5  ?  19.732  26.010    1.411  1.00  78.59  ?   17  DA   B  "O3'"  1  
+ATOM    332  C  "C2'"  .  DA   B  1   5  ?  19.752  23.945    2.791  1.00  44.42  ?   17  DA   B  "C2'"  1  
+ATOM    333  C  "C1'"  .  DA   B  1   5  ?  18.497  23.393    2.145  1.00  42.55  ?   17  DA   B  "C1'"  1  
+ATOM    334  N  N9     .  DA   B  1   5  ?  18.079  22.095    2.758  1.00  34.56  ?   17  DA   B  N9     1  
+ATOM    335  C  C8     .  DA   B  1   5  ?  18.847  21.020    3.133  1.00  20.07  ?   17  DA   B  C8     1  
+ATOM    336  N  N7     .  DA   B  1   5  ?  18.114  19.984    3.584  1.00  27.60  ?   17  DA   B  N7     1  
+ATOM    337  C  C5     .  DA   B  1   5  ?  16.842  20.424    3.488  1.00  18.80  ?   17  DA   B  C5     1  
+ATOM    338  C  C6     .  DA   B  1   5  ?  15.577  19.817    3.786  1.00  32.58  ?   17  DA   B  C6     1  
+ATOM    339  N  N6     .  DA   B  1   5  ?  15.448  18.537    4.242  1.00  29.54  ?   17  DA   B  N6     1  
+ATOM    340  N  N1     .  DA   B  1   5  ?  14.482  20.557    3.593  1.00  35.01  ?   17  DA   B  N1     1  
+ATOM    341  C  C2     .  DA   B  1   5  ?  14.597  21.801    3.118  1.00  36.47  ?   17  DA   B  C2     1  
+ATOM    342  N  N3     .  DA   B  1   5  ?  15.700  22.472    2.783  1.00  38.96  ?   17  DA   B  N3     1  
+ATOM    343  C  C4     .  DA   B  1   5  ?  16.791  21.706    3.002  1.00  28.24  ?   17  DA   B  C4     1  
+ATOM    344  P  P      .  DA   B  1   6  ?  19.803  27.141    2.526  1.00  46.11  ?   18  DA   B  P      1  
+ATOM    345  O  OP1    .  DA   B  1   6  ?  19.796  28.478    1.888  1.00  49.20  ?   18  DA   B  OP1    1  
+ATOM    346  O  OP2    .  DA   B  1   6  ?  20.953  26.858    3.426  1.00  43.48  ?   18  DA   B  OP2    1  
+ATOM    347  O  "O5'"  .  DA   B  1   6  ?  18.396  26.939    3.241  1.00  40.83  ?   18  DA   B  "O5'"  1  
+ATOM    348  C  "C5'"  .  DA   B  1   6  ?  17.203  27.028    2.452  1.00  40.72  ?   18  DA   B  "C5'"  1  
+ATOM    349  C  "C4'"  .  DA   B  1   6  ?  16.035  26.958    3.388  1.00  66.52  ?   18  DA   B  "C4'"  1  
+ATOM    350  O  "O4'"  .  DA   B  1   6  ?  15.856  25.612    3.850  1.00  44.25  ?   18  DA   B  "O4'"  1  
+ATOM    351  C  "C3'"  .  DA   B  1   6  ?  16.101  27.861    4.615  1.00  63.34  ?   18  DA   B  "C3'"  1  
+ATOM    352  O  "O3'"  .  DA   B  1   6  ?  14.890  28.608    4.757  1.00  55.65  ?   18  DA   B  "O3'"  1  
+ATOM    353  C  "C2'"  .  DA   B  1   6  ?  16.368  26.844    5.724  1.00  34.49  ?   18  DA   B  "C2'"  1  
+ATOM    354  C  "C1'"  .  DA   B  1   6  ?  15.561  25.655    5.243  1.00  29.45  ?   18  DA   B  "C1'"  1  
+ATOM    355  N  N9     .  DA   B  1   6  ?  16.104  24.373    5.755  1.00  20.03  ?   18  DA   B  N9     1  
+ATOM    356  C  C8     .  DA   B  1   6  ?  17.411  23.967    5.830  1.00  16.51  ?   18  DA   B  C8     1  
+ATOM    357  N  N7     .  DA   B  1   6  ?  17.539  22.706    6.276  1.00  20.58  ?   18  DA   B  N7     1  
+ATOM    358  C  C5     .  DA   B  1   6  ?  16.266  22.309    6.480  1.00  21.66  ?   18  DA   B  C5     1  
+ATOM    359  C  C6     .  DA   B  1   6  ?  15.715  21.073    6.933  1.00  17.93  ?   18  DA   B  C6     1  
+ATOM    360  N  N6     .  DA   B  1   6  ?  16.483  19.994    7.243  1.00  20.37  ?   18  DA   B  N6     1  
+ATOM    361  N  N1     .  DA   B  1   6  ?  14.389  20.994    7.036  1.00  20.81  ?   18  DA   B  N1     1  
+ATOM    362  C  C2     .  DA   B  1   6  ?  13.636  22.041    6.708  1.00  26.77  ?   18  DA   B  C2     1  
+ATOM    363  N  N3     .  DA   B  1   6  ?  14.019  23.234    6.265  1.00  26.83  ?   18  DA   B  N3     1  
+ATOM    364  C  C4     .  DA   B  1   6  ?  15.367  23.291    6.174  1.00  27.48  ?   18  DA   B  C4     1  
+ATOM    365  P  P      .  DT   B  1   7  ?  14.604  29.545    6.020  1.00  48.40  ?   19  DT   B  P      1  
+ATOM    366  O  OP1    .  DT   B  1   7  ?  13.792  30.696    5.582  1.00  50.18  ?   19  DT   B  OP1    1  
+ATOM    367  O  OP2    .  DT   B  1   7  ?  15.852  29.836    6.749  1.00  44.42  ?   19  DT   B  OP2    1  
+ATOM    368  O  "O5'"  .  DT   B  1   7  ?  13.633  28.628    6.885  1.00  53.86  ?   19  DT   B  "O5'"  1  
+ATOM    369  C  "C5'"  .  DT   B  1   7  ?  12.398  28.171    6.303  1.00  55.04  ?   19  DT   B  "C5'"  1  
+ATOM    370  C  "C4'"  .  DT   B  1   7  ?  11.809  27.217    7.302  1.00  44.86  ?   19  DT   B  "C4'"  1  
+ATOM    371  O  "O4'"  .  DT   B  1   7  ?  12.767  26.184    7.534  1.00  48.52  ?   19  DT   B  "O4'"  1  
+ATOM    372  C  "C3'"  .  DT   B  1   7  ?  11.515  27.822    8.669  1.00  41.77  ?   19  DT   B  "C3'"  1  
+ATOM    373  O  "O3'"  .  DT   B  1   7  ?  10.103  27.952    8.891  1.00  57.02  ?   19  DT   B  "O3'"  1  
+ATOM    374  C  "C2'"  .  DT   B  1   7  ?  12.267  26.906    9.630  1.00  39.28  ?   19  DT   B  "C2'"  1  
+ATOM    375  C  "C1'"  .  DT   B  1   7  ?  12.426  25.645    8.799  1.00  27.68  ?   19  DT   B  "C1'"  1  
+ATOM    376  N  N1     .  DT   B  1   7  ?  13.609  24.850    9.205  1.00  21.67  ?   19  DT   B  N1     1  
+ATOM    377  C  C2     .  DT   B  1   7  ?  13.442  23.575    9.656  1.00  31.71  ?   19  DT   B  C2     1  
+ATOM    378  O  O2     .  DT   B  1   7  ?  12.311  23.101    9.802  1.00  36.00  ?   19  DT   B  O2     1  
+ATOM    379  N  N3     .  DT   B  1   7  ?  14.551  22.825    9.913  1.00  24.66  ?   19  DT   B  N3     1  
+ATOM    380  C  C4     .  DT   B  1   7  ?  15.815  23.321    9.777  1.00  40.64  ?   19  DT   B  C4     1  
+ATOM    381  O  O4     .  DT   B  1   7  ?  16.755  22.570   10.029  1.00  31.47  ?   19  DT   B  O4     1  
+ATOM    382  C  C5     .  DT   B  1   7  ?  15.972  24.647    9.362  1.00  31.79  ?   19  DT   B  C5     1  
+ATOM    383  C  C7     .  DT   B  1   7  ?  17.345  25.239    9.234  1.00  30.05  ?   19  DT   B  C7     1  
+ATOM    384  C  C6     .  DT   B  1   7  ?  14.844  25.405    9.048  1.00  14.35  ?   19  DT   B  C6     1  
+ATOM    385  P  P      .  DT   B  1   8  ?   9.513  28.533   10.260  1.00  48.24  ?   20  DT   B  P      1  
+ATOM    386  O  OP1    .  DT   B  1   8  ?   8.145  29.007    9.998  1.00  41.28  ?   20  DT   B  OP1    1  
+ATOM    387  O  OP2    .  DT   B  1   8  ?  10.455  29.513   10.841  1.00  53.39  ?   20  DT   B  OP2    1  
+ATOM    388  O  "O5'"  .  DT   B  1   8  ?   9.395  27.223   11.153  1.00  36.57  ?   20  DT   B  "O5'"  1  
+ATOM    389  C  "C5'"  .  DT   B  1   8  ?   8.576  26.148   10.664  1.00  50.41  ?   20  DT   B  "C5'"  1  
+ATOM    390  C  "C4'"  .  DT   B  1   8  ?   8.655  25.060   11.678  1.00  32.08  ?   20  DT   B  "C4'"  1  
+ATOM    391  O  "O4'"  .  DT   B  1   8  ?  10.003  24.615   11.764  1.00  48.38  ?   20  DT   B  "O4'"  1  
+ATOM    392  C  "C3'"  .  DT   B  1   8  ?   8.272  25.471   13.087  1.00  29.99  ?   20  DT   B  "C3'"  1  
+ATOM    393  O  "O3'"  .  DT   B  1   8  ?   7.199  24.657   13.553  1.00  45.14  ?   20  DT   B  "O3'"  1  
+ATOM    394  C  "C2'"  .  DT   B  1   8  ?   9.586  25.307   13.860  1.00  32.42  ?   20  DT   B  "C2'"  1  
+ATOM    395  C  "C1'"  .  DT   B  1   8  ?  10.190  24.148   13.089  1.00  39.56  ?   20  DT   B  "C1'"  1  
+ATOM    396  N  N1     .  DT   B  1   8  ?  11.660  24.070   13.205  1.00  20.36  ?   20  DT   B  N1     1  
+ATOM    397  C  C2     .  DT   B  1   8  ?  12.257  22.880   13.486  1.00  27.55  ?   20  DT   B  C2     1  
+ATOM    398  O  O2     .  DT   B  1   8  ?  11.583  21.866   13.691  1.00  38.33  ?   20  DT   B  O2     1  
+ATOM    399  N  N3     .  DT   B  1   8  ?  13.620  22.829   13.497  1.00  29.60  ?   20  DT   B  N3     1  
+ATOM    400  C  C4     .  DT   B  1   8  ?  14.402  23.914   13.225  1.00  30.11  ?   20  DT   B  C4     1  
+ATOM    401  O  O4     .  DT   B  1   8  ?  15.625  23.764   13.252  1.00  32.92  ?   20  DT   B  O4     1  
+ATOM    402  C  C5     .  DT   B  1   8  ?  13.774  25.126   12.933  1.00  24.11  ?   20  DT   B  C5     1  
+ATOM    403  C  C7     .  DT   B  1   8  ?  14.563  26.358   12.612  1.00  23.96  ?   20  DT   B  C7     1  
+ATOM    404  C  C6     .  DT   B  1   8  ?  12.385  25.187   12.926  1.00  19.78  ?   20  DT   B  C6     1  
+ATOM    405  P  P      .  DC   B  1   9  ?   6.594  24.823   15.016  1.00  54.73  ?   21  DC   B  P      1  
+ATOM    406  O  OP1    .  DC   B  1   9  ?   5.169  24.424   14.987  1.00  53.98  ?   21  DC   B  OP1    1  
+ATOM    407  O  OP2    .  DC   B  1   9  ?   6.870  26.189   15.511  1.00  65.53  ?   21  DC   B  OP2    1  
+ATOM    408  O  "O5'"  .  DC   B  1   9  ?   7.409  23.731   15.839  1.00  50.67  ?   21  DC   B  "O5'"  1  
+ATOM    409  C  "C5'"  .  DC   B  1   9  ?   7.331  22.352   15.433  1.00  60.86  ?   21  DC   B  "C5'"  1  
+ATOM    410  C  "C4'"  .  DC   B  1   9  ?   8.100  21.598   16.461  1.00  40.86  ?   21  DC   B  "C4'"  1  
+ATOM    411  O  "O4'"  .  DC   B  1   9  ?   9.478  21.902   16.263  1.00  36.88  ?   21  DC   B  "O4'"  1  
+ATOM    412  C  "C3'"  .  DC   B  1   9  ?   7.766  22.045   17.879  1.00  53.80  ?   21  DC   B  "C3'"  1  
+ATOM    413  O  "O3'"  .  DC   B  1   9  ?   7.036  21.041   18.611  1.00  79.04  ?   21  DC   B  "O3'"  1  
+ATOM    414  C  "C2'"  .  DC   B  1   9  ?   9.123  22.414   18.469  1.00  48.43  ?   21  DC   B  "C2'"  1  
+ATOM    415  C  "C1'"  .  DC   B  1   9  ?  10.107  21.743   17.523  1.00  36.51  ?   21  DC   B  "C1'"  1  
+ATOM    416  N  N1     .  DC   B  1   9  ?  11.328  22.556   17.331  1.00  24.72  ?   21  DC   B  N1     1  
+ATOM    417  C  C2     .  DC   B  1   9  ?  12.534  21.939   17.329  1.00  30.96  ?   21  DC   B  C2     1  
+ATOM    418  O  O2     .  DC   B  1   9  ?  12.560  20.731   17.579  1.00  34.53  ?   21  DC   B  O2     1  
+ATOM    419  N  N3     .  DC   B  1   9  ?  13.639  22.639   17.035  1.00  31.69  ?   21  DC   B  N3     1  
+ATOM    420  C  C4     .  DC   B  1   9  ?  13.560  23.938   16.739  1.00  21.53  ?   21  DC   B  C4     1  
+ATOM    421  N  N4     .  DC   B  1   9  ?  14.685  24.628   16.404  1.00  23.72  ?   21  DC   B  N4     1  
+ATOM    422  C  C5     .  DC   B  1   9  ?  12.338  24.609   16.736  1.00  30.74  ?   21  DC   B  C5     1  
+ATOM    423  C  C6     .  DC   B  1   9  ?  11.193  23.878   17.035  1.00  27.58  ?   21  DC   B  C6     1  
+ATOM    424  P  P      .  DG   B  1  10  ?   6.509  21.324   20.099  1.00  56.50  ?   22  DG   B  P      1  
+ATOM    425  O  OP1    .  DG   B  1  10  ?   5.387  20.397   20.396  1.00  50.81  ?   22  DG   B  OP1    1  
+ATOM    426  O  OP2    .  DG   B  1  10  ?   6.235  22.774   20.306  1.00  53.84  ?   22  DG   B  OP2    1  
+ATOM    427  O  "O5'"  .  DG   B  1  10  ?   7.767  20.924   20.993  1.00  66.30  ?   22  DG   B  "O5'"  1  
+ATOM    428  C  "C5'"  .  DG   B  1  10  ?   8.216  19.559   21.073  1.00  73.42  ?   22  DG   B  "C5'"  1  
+ATOM    429  C  "C4'"  .  DG   B  1  10  ?   9.422  19.557   21.977  1.00  42.96  ?   22  DG   B  "C4'"  1  
+ATOM    430  O  "O4'"  .  DG   B  1  10  ?  10.493  20.260   21.319  1.00  52.87  ?   22  DG   B  "O4'"  1  
+ATOM    431  C  "C3'"  .  DG   B  1  10  ?   9.267  20.267   23.325  1.00  38.51  ?   22  DG   B  "C3'"  1  
+ATOM    432  O  "O3'"  .  DG   B  1  10  ?  10.088  19.657   24.293  1.00  60.28  ?   22  DG   B  "O3'"  1  
+ATOM    433  C  "C2'"  .  DG   B  1  10  ?   9.751  21.670   22.990  1.00  22.00  ?   22  DG   B  "C2'"  1  
+ATOM    434  C  "C1'"  .  DG   B  1  10  ?  10.988  21.226   22.256  1.00  24.85  ?   22  DG   B  "C1'"  1  
+ATOM    435  N  N9     .  DG   B  1  10  ?  11.599  22.357   21.543  1.00  25.91  ?   22  DG   B  N9     1  
+ATOM    436  C  C8     .  DG   B  1  10  ?  11.037  23.545   21.159  1.00  23.91  ?   22  DG   B  C8     1  
+ATOM    437  N  N7     .  DG   B  1  10  ?  11.921  24.362   20.566  1.00  39.18  ?   22  DG   B  N7     1  
+ATOM    438  C  C5     .  DG   B  1  10  ?  13.072  23.653   20.580  1.00  25.66  ?   22  DG   B  C5     1  
+ATOM    439  C  C6     .  DG   B  1  10  ?  14.370  24.003   20.102  1.00  28.34  ?   22  DG   B  C6     1  
+ATOM    440  O  O6     .  DG   B  1  10  ?  14.747  25.057   19.585  1.00  31.85  ?   22  DG   B  O6     1  
+ATOM    441  N  N1     .  DG   B  1  10  ?  15.268  22.983   20.308  1.00  25.22  ?   22  DG   B  N1     1  
+ATOM    442  C  C2     .  DG   B  1  10  ?  15.023  21.776   20.891  1.00  11.07  ?   22  DG   B  C2     1  
+ATOM    443  N  N2     .  DG   B  1  10  ?  16.066  20.914   21.038  1.00  25.92  ?   22  DG   B  N2     1  
+ATOM    444  N  N3     .  DG   B  1  10  ?  13.815  21.452   21.350  1.00  19.05  ?   22  DG   B  N3     1  
+ATOM    445  C  C4     .  DG   B  1  10  ?  12.902  22.429   21.151  1.00  23.69  ?   22  DG   B  C4     1  
+ATOM    446  P  P      .  DC   B  1  11  ?   9.477  18.627   25.340  1.00  55.93  ?   23  DC   B  P      1  
+ATOM    447  O  OP1    .  DC   B  1  11  ?   8.767  17.534   24.627  1.00  45.14  ?   23  DC   B  OP1    1  
+ATOM    448  O  OP2    .  DC   B  1  11  ?   8.670  19.409   26.312  1.00  41.61  ?   23  DC   B  OP2    1  
+ATOM    449  O  "O5'"  .  DC   B  1  11  ?  10.807  18.067   26.034  1.00  59.70  ?   23  DC   B  "O5'"  1  
+ATOM    450  C  "C5'"  .  DC   B  1  11  ?  11.688  17.170   25.310  1.00  63.13  ?   23  DC   B  "C5'"  1  
+ATOM    451  C  "C4'"  .  DC   B  1  11  ?  13.115  17.573   25.593  1.00  27.86  ?   23  DC   B  "C4'"  1  
+ATOM    452  O  "O4'"  .  DC   B  1  11  ?  13.284  18.804   24.893  1.00  50.51  ?   23  DC   B  "O4'"  1  
+ATOM    453  C  "C3'"  .  DC   B  1  11  ?  13.441  17.879   27.059  1.00  46.45  ?   23  DC   B  "C3'"  1  
+ATOM    454  O  "O3'"  .  DC   B  1  11  ?  14.341  16.938   27.677  1.00  57.21  ?   23  DC   B  "O3'"  1  
+ATOM    455  C  "C2'"  .  DC   B  1  11  ?  13.928  19.322   27.025  1.00  68.01  ?   23  DC   B  "C2'"  1  
+ATOM    456  C  "C1'"  .  DC   B  1  11  ?  14.312  19.508   25.568  1.00  32.05  ?   23  DC   B  "C1'"  1  
+ATOM    457  N  N1     .  DC   B  1  11  ?  14.144  20.932   25.170  1.00  23.28  ?   23  DC   B  N1     1  
+ATOM    458  C  C2     .  DC   B  1  11  ?  15.199  21.595   24.630  1.00  20.62  ?   23  DC   B  C2     1  
+ATOM    459  O  O2     .  DC   B  1  11  ?  16.257  20.984   24.504  1.00  29.62  ?   23  DC   B  O2     1  
+ATOM    460  N  N3     .  DC   B  1  11  ?  15.067  22.877   24.257  1.00  39.00  ?   23  DC   B  N3     1  
+ATOM    461  C  C4     .  DC   B  1  11  ?  13.898  23.510   24.404  1.00  30.44  ?   23  DC   B  C4     1  
+ATOM    462  N  N4     .  DC   B  1  11  ?  13.771  24.813   24.018  1.00  34.66  ?   23  DC   B  N4     1  
+ATOM    463  C  C5     .  DC   B  1  11  ?  12.795  22.866   24.967  1.00  27.74  ?   23  DC   B  C5     1  
+ATOM    464  C  C6     .  DC   B  1  11  ?  12.935  21.540   25.359  1.00  24.58  ?   23  DC   B  C6     1  
+ATOM    465  P  P      .  DG   B  1  12  ?  14.658  17.064   29.247  1.00  53.70  ?   24  DG   B  P      1  
+ATOM    466  O  OP1    .  DG   B  1  12  ?  14.863  15.717   29.825  1.00  61.79  ?   24  DG   B  OP1    1  
+ATOM    467  O  OP2    .  DG   B  1  12  ?  13.633  17.912   29.920  1.00  36.06  ?   24  DG   B  OP2    1  
+ATOM    468  O  "O5'"  .  DG   B  1  12  ?  16.033  17.880   29.284  1.00  34.06  ?   24  DG   B  "O5'"  1  
+ATOM    469  C  "C5'"  .  DG   B  1  12  ?  17.243  17.320   28.742  1.00  46.57  ?   24  DG   B  "C5'"  1  
+ATOM    470  C  "C4'"  .  DG   B  1  12  ?  18.208  18.464   28.758  1.00  50.89  ?   24  DG   B  "C4'"  1  
+ATOM    471  O  "O4'"  .  DG   B  1  12  ?  17.716  19.428   27.829  1.00  32.02  ?   24  DG   B  "O4'"  1  
+ATOM    472  C  "C3'"  .  DG   B  1  12  ?  18.230  19.236   30.058  1.00  30.38  ?   24  DG   B  "C3'"  1  
+ATOM    473  O  "O3'"  .  DG   B  1  12  ?  18.978  18.583   31.084  1.00  61.06  ?   24  DG   B  "O3'"  1  
+ATOM    474  C  "C2'"  .  DG   B  1  12  ?  18.885  20.519   29.578  1.00  53.33  ?   24  DG   B  "C2'"  1  
+ATOM    475  C  "C1'"  .  DG   B  1  12  ?  18.276  20.693   28.188  1.00  35.03  ?   24  DG   B  "C1'"  1  
+ATOM    476  N  N9     .  DG   B  1  12  ?  17.164  21.659   28.139  1.00  30.25  ?   24  DG   B  N9     1  
+ATOM    477  C  C8     .  DG   B  1  12  ?  15.874  21.536   28.580  1.00  30.86  ?   24  DG   B  C8     1  
+ATOM    478  N  N7     .  DG   B  1  12  ?  15.129  22.614   28.308  1.00  44.08  ?   24  DG   B  N7     1  
+ATOM    479  C  C5     .  DG   B  1  12  ?  15.990  23.436   27.673  1.00  16.87  ?   24  DG   B  C5     1  
+ATOM    480  C  C6     .  DG   B  1  12  ?  15.765  24.729   27.117  1.00  19.36  ?   24  DG   B  C6     1  
+ATOM    481  O  O6     .  DG   B  1  12  ?  14.719  25.373   27.067  1.00  33.30  ?   24  DG   B  O6     1  
+ATOM    482  N  N1     .  DG   B  1  12  ?  16.926  25.257   26.604  1.00  15.78  ?   24  DG   B  N1     1  
+ATOM    483  C  C2     .  DG   B  1  12  ?  18.157  24.666   26.579  1.00  11.92  ?   24  DG   B  C2     1  
+ATOM    484  N  N2     .  DG   B  1  12  ?  19.208  25.386   26.096  1.00  29.76  ?   24  DG   B  N2     1  
+ATOM    485  N  N3     .  DG   B  1  12  ?  18.350  23.438   27.053  1.00  21.95  ?   24  DG   B  N3     1  
+ATOM    486  C  C4     .  DG   B  1  12  ?  17.231  22.893   27.570  1.00  13.89  ?   24  DG   B  C4     1  
+HETATM  487  O  O      .  HOH  C  2   .  ?  19.736  30.706   18.656  1.00  51.86  ?   25  HOH  A  O      1  
+HETATM  488  O  O      .  HOH  C  2   .  ?  10.879  26.039   -8.906  1.00  47.07  ?   31  HOH  A  O      1  
+HETATM  489  O  O      .  HOH  C  2   .  ?  18.320  24.816   14.948  1.00  47.72  ?   32  HOH  A  O      1  
+HETATM  490  O  O      .  HOH  C  2   .  ?   9.821  13.442    8.572  1.00  45.76  ?   36  HOH  A  O      1  
+HETATM  491  O  O      .  HOH  C  2   .  ?   8.915  15.602   -3.388  1.00  50.97  ?   38  HOH  A  O      1  
+HETATM  492  O  O      .  HOH  C  2   .  ?  17.505  26.340  -10.581  1.00  51.90  ?   39  HOH  A  O      1  
+HETATM  493  O  O      .  HOH  C  2   .  ?  28.496  23.515   18.349  1.00  45.37  ?   40  HOH  A  O      1  
+HETATM  494  O  O      .  HOH  C  2   .  ?  11.346  24.175    4.920  1.00  45.03  ?   41  HOH  A  O      1  
+HETATM  495  O  O      .  HOH  C  2   .  ?   9.098  16.119    1.277  1.00  51.80  ?   50  HOH  A  O      1  
+HETATM  496  O  O      .  HOH  C  2   .  ?  16.488  29.195   19.861  1.00  54.92  ?   54  HOH  A  O      1  
+HETATM  497  O  O      .  HOH  C  2   .  ?  22.078  25.894   15.396  1.00  62.20  ?   55  HOH  A  O      1  
+HETATM  498  O  O      .  HOH  C  2   .  ?   7.133  14.448    4.647  1.00  57.15  ?   58  HOH  A  O      1  
+HETATM  499  O  O      .  HOH  C  2   .  ?  14.095  28.151   21.614  1.00  53.85  ?   62  HOH  A  O      1  
+HETATM  500  O  O      .  HOH  C  2   .  ?  27.164  31.710   20.331  1.00  56.84  ?   64  HOH  A  O      1  
+HETATM  501  O  O      .  HOH  C  2   .  ?  15.295  11.873   12.209  1.00  57.34  ?   65  HOH  A  O      1  
+HETATM  502  O  O      .  HOH  C  2   .  ?  18.180  16.604    9.966  1.00  61.52  ?   66  HOH  A  O      1  
+HETATM  503  O  O      .  HOH  C  2   .  ?   6.216  17.035    1.672  1.00  62.91  ?   67  HOH  A  O      1  
+HETATM  504  O  O      .  HOH  C  2   .  ?   7.055  25.519   -2.053  1.00  55.96  ?   70  HOH  A  O      1  
+HETATM  505  O  O      .  HOH  C  2   .  ?  12.454  11.354    9.415  1.00  68.40  ?   74  HOH  A  O      1  
+HETATM  506  O  O      .  HOH  C  2   .  ?  11.492  29.103   20.090  1.00  67.46  ?   76  HOH  A  O      1  
+HETATM  507  O  O      .  HOH  C  2   .  ?  14.220  29.189   20.392  1.00  48.22  ?   77  HOH  A  O      1  
+HETATM  508  O  O      .  HOH  C  2   .  ?   6.138  19.149   13.844  1.00  62.26  ?   78  HOH  A  O      1  
+HETATM  509  O  O      .  HOH  C  2   .  ?  17.315   9.638   13.392  1.00  65.70  ?   79  HOH  A  O      1  
+HETATM  510  O  O      .  HOH  C  2   .  ?  18.951  25.757   12.989  1.00  66.47  ?   80  HOH  A  O      1  
+HETATM  511  O  O      .  HOH  C  2   .  ?  20.460  18.861   12.664  1.00  63.00  ?   81  HOH  A  O      1  
+HETATM  512  O  O      .  HOH  C  2   .  ?   3.529  19.338   12.599  1.00  65.32  ?   82  HOH  A  O      1  
+HETATM  513  O  O      .  HOH  C  2   .  ?  16.223  12.351    9.406  1.00  63.59  ?   84  HOH  A  O      1  
+HETATM  514  O  O      .  HOH  C  2   .  ?  12.989  29.901   -9.282  1.00  64.97  ?   85  HOH  A  O      1  
+HETATM  515  O  O      .  HOH  C  2   .  ?  17.510  30.569   18.702  1.00  61.79  ?   86  HOH  A  O      1  
+HETATM  516  O  O      .  HOH  C  2   .  ?  25.377  12.891   19.011  1.00  73.80  ?   87  HOH  A  O      1  
+HETATM  517  O  O      .  HOH  C  2   .  ?  13.610  15.742   18.593  1.00  69.48  ?   88  HOH  A  O      1  
+HETATM  518  O  O      .  HOH  C  2   .  ?  18.012  32.598   15.262  1.00  67.52  ?   89  HOH  A  O      1  
+HETATM  519  O  O      .  HOH  C  2   .  ?   8.723  13.216    6.359  1.00  70.66  ?   92  HOH  A  O      1  
+HETATM  520  O  O      .  HOH  C  2   .  ?  18.779  13.814   11.704  1.00  71.14  ?   97  HOH  A  O      1  
+HETATM  521  O  O      .  HOH  C  2   .  ?  12.227  25.192  -10.299  1.00  70.46  ?   99  HOH  A  O      1  
+HETATM  522  O  O      .  HOH  C  2   .  ?  12.292  30.291   27.102  1.00  73.04  ?  100  HOH  A  O      1  
+HETATM  523  O  O      .  HOH  C  2   .  ?  20.170  23.000   12.999  1.00  73.63  ?  102  HOH  A  O      1  
+HETATM  524  O  O      .  HOH  D  2   .  ?  14.354  27.683   16.369  1.00  40.92  ?   26  HOH  B  O      1  
+HETATM  525  O  O      .  HOH  D  2   .  ?   9.864  22.509    9.123  1.00  39.67  ?   27  HOH  B  O      1  
+HETATM  526  O  O      .  HOH  D  2   .  ?  19.526  19.144    7.481  1.00  51.15  ?   28  HOH  B  O      1  
+HETATM  527  O  O      .  HOH  D  2   .  ?  25.754  12.744   -1.835  1.00  51.80  ?   29  HOH  B  O      1  
+HETATM  528  O  O      .  HOH  D  2   .  ?   7.478  20.604   -9.000  1.00  44.82  ?   30  HOH  B  O      1  
+HETATM  529  O  O      .  HOH  D  2   .  ?   9.012  24.586    7.009  1.00  43.42  ?   33  HOH  B  O      1  
+HETATM  530  O  O      .  HOH  D  2   .  ?  10.152  19.917   13.381  1.00  48.04  ?   34  HOH  B  O      1  
+HETATM  531  O  O      .  HOH  D  2   .  ?   7.764  21.397   11.075  1.00  41.41  ?   35  HOH  B  O      1  
+HETATM  532  O  O      .  HOH  D  2   .  ?  13.239  14.428    2.049  1.00  55.54  ?   37  HOH  B  O      1  
+HETATM  533  O  O      .  HOH  D  2   .  ?  12.601  23.000   29.167  1.00  51.36  ?   42  HOH  B  O      1  
+HETATM  534  O  O      .  HOH  D  2   .  ?  10.440  25.542   24.443  1.00  56.79  ?   43  HOH  B  O      1  
+HETATM  535  O  O      .  HOH  D  2   .  ?  16.979  28.689   16.284  1.00  50.41  ?   44  HOH  B  O      1  
+HETATM  536  O  O      .  HOH  D  2   .  ?   4.794  22.966   13.368  1.00  45.95  ?   45  HOH  B  O      1  
+HETATM  537  O  O      .  HOH  D  2   .  ?   4.208  25.591   10.828  1.00  51.06  ?   46  HOH  B  O      1  
+HETATM  538  O  O      .  HOH  D  2   .  ?   6.362  24.374    9.188  1.00  51.85  ?   47  HOH  B  O      1  
+HETATM  539  O  O      .  HOH  D  2   .  ?   7.688  28.411    7.883  1.00  49.33  ?   48  HOH  B  O      1  
+HETATM  540  O  O      .  HOH  D  2   .  ?  18.379  17.074    4.809  1.00  50.72  ?   49  HOH  B  O      1  
+HETATM  541  O  O      .  HOH  D  2   .  ?  26.464  23.826    1.396  1.00  53.21  ?   51  HOH  B  O      1  
+HETATM  542  O  O      .  HOH  D  2   .  ?  11.014  11.318   -2.909  1.00  51.36  ?   52  HOH  B  O      1  
+HETATM  543  O  O      .  HOH  D  2   .  ?   9.476  27.782   26.498  1.00  60.04  ?   53  HOH  B  O      1  
+HETATM  544  O  O      .  HOH  D  2   .  ?   5.522  27.411    9.017  1.00  62.36  ?   56  HOH  B  O      1  
+HETATM  545  O  O      .  HOH  D  2   .  ?  18.456  28.409    8.821  1.00  59.63  ?   57  HOH  B  O      1  
+HETATM  546  O  O      .  HOH  D  2   .  ?  22.610  15.544    3.846  1.00  57.52  ?   59  HOH  B  O      1  
+HETATM  547  O  O      .  HOH  D  2   .  ?  24.407  13.162    2.229  1.00  52.30  ?   60  HOH  B  O      1  
+HETATM  548  O  O      .  HOH  D  2   .  ?   7.988  11.556   -2.976  1.00  59.14  ?   61  HOH  B  O      1  
+HETATM  549  O  O      .  HOH  D  2   .  ?  14.213  27.722   18.905  1.00  57.29  ?   63  HOH  B  O      1  
+HETATM  550  O  O      .  HOH  D  2   .  ?  19.101  11.433    1.080  1.00  59.79  ?   68  HOH  B  O      1  
+HETATM  551  O  O      .  HOH  D  2   .  ?  12.607  10.967    0.261  1.00  60.87  ?   69  HOH  B  O      1  
+HETATM  552  O  O      .  HOH  D  2   .  ?  15.062  26.024   -0.766  1.00  56.35  ?   71  HOH  B  O      1  
+HETATM  553  O  O      .  HOH  D  2   .  ?  16.380   6.413   -4.784  1.00  59.07  ?   72  HOH  B  O      1  
+HETATM  554  O  O      .  HOH  D  2   .  ?  14.059   5.751   -6.198  1.00  56.68  ?   73  HOH  B  O      1  
+HETATM  555  O  O      .  HOH  D  2   .  ?   9.613  17.039   29.793  1.00  63.48  ?   75  HOH  B  O      1  
+HETATM  556  O  O      .  HOH  D  2   .  ?  25.276  15.890   -1.301  1.00  64.53  ?   83  HOH  B  O      1  
+HETATM  557  O  O      .  HOH  D  2   .  ?   2.622  23.030   10.332  1.00  68.01  ?   90  HOH  B  O      1  
+HETATM  558  O  O      .  HOH  D  2   .  ?  19.701  22.518    9.511  1.00  70.25  ?   91  HOH  B  O      1  
+HETATM  559  O  O      .  HOH  D  2   .  ?  19.727  29.488    6.155  1.00  69.43  ?   93  HOH  B  O      1  
+HETATM  560  O  O      .  HOH  D  2   .  ?  17.241  11.563    4.511  1.00  72.18  ?   94  HOH  B  O      1  
+HETATM  561  O  O      .  HOH  D  2   .  ?  26.545  19.404   -1.091  1.00  70.14  ?   95  HOH  B  O      1  
+HETATM  562  O  O      .  HOH  D  2   .  ?   9.697  18.315   14.885  1.00  69.10  ?   96  HOH  B  O      1  
+HETATM  563  O  O      .  HOH  D  2   .  ?  14.292  25.159    2.287  1.00  68.44  ?   98  HOH  B  O      1  
+HETATM  564  O  O      .  HOH  D  2   .  ?   9.396  27.092   16.993  1.00  72.98  ?  101  HOH  B  O      1  
+HETATM  565  O  O      .  HOH  D  2   .  ?  19.987  21.691    6.802  1.00  72.66  ?  103  HOH  B  O      1  
+HETATM  566  O  O      .  HOH  D  2   .  ?  18.692  31.584    4.596  1.00  72.98  ?  104  HOH  B  O      1  
+##
+loop_
+_pdbx_poly_seq_scheme.asym_id
+_pdbx_poly_seq_scheme.entity_id
+_pdbx_poly_seq_scheme.seq_id
+_pdbx_poly_seq_scheme.mon_id
+_pdbx_poly_seq_scheme.ndb_seq_num
+_pdbx_poly_seq_scheme.pdb_seq_num
+_pdbx_poly_seq_scheme.auth_seq_num
+_pdbx_poly_seq_scheme.pdb_mon_id
+_pdbx_poly_seq_scheme.auth_mon_id
+_pdbx_poly_seq_scheme.pdb_strand_id
+_pdbx_poly_seq_scheme.pdb_ins_code
+_pdbx_poly_seq_scheme.hetero
+A  1   1  DC   1   1   1  DC  C  A  .  n  
+A  1   2  DG   2   2   2  DG  G  A  .  n  
+A  1   3  DC   3   3   3  DC  C  A  .  n  
+A  1   4  DG   4   4   4  DG  G  A  .  n  
+A  1   5  DA   5   5   5  DA  A  A  .  n  
+A  1   6  DA   6   6   6  DA  A  A  .  n  
+A  1   7  DT   7   7   7  DT  T  A  .  n  
+A  1   8  DT   8   8   8  DT  T  A  .  n  
+A  1   9  DC   9   9   9  DC  C  A  .  n  
+A  1  10  DG  10  10  10  DG  G  A  .  n  
+A  1  11  DC  11  11  11  DC  C  A  .  n  
+A  1  12  DG  12  12  12  DG  G  A  .  n  
+B  1   1  DC   1  13  13  DC  C  B  .  n  
+B  1   2  DG   2  14  14  DG  G  B  .  n  
+B  1   3  DC   3  15  15  DC  C  B  .  n  
+B  1   4  DG   4  16  16  DG  G  B  .  n  
+B  1   5  DA   5  17  17  DA  A  B  .  n  
+B  1   6  DA   6  18  18  DA  A  B  .  n  
+B  1   7  DT   7  19  19  DT  T  B  .  n  
+B  1   8  DT   8  20  20  DT  T  B  .  n  
+B  1   9  DC   9  21  21  DC  C  B  .  n  
+B  1  10  DG  10  22  22  DG  G  B  .  n  
+B  1  11  DC  11  23  23  DC  C  B  .  n  
+B  1  12  DG  12  24  24  DG  G  B  .  n  
+##
+loop_
+_pdbx_nonpoly_scheme.asym_id
+_pdbx_nonpoly_scheme.entity_id
+_pdbx_nonpoly_scheme.mon_id
+_pdbx_nonpoly_scheme.ndb_seq_num
+_pdbx_nonpoly_scheme.pdb_seq_num
+_pdbx_nonpoly_scheme.auth_seq_num
+_pdbx_nonpoly_scheme.pdb_mon_id
+_pdbx_nonpoly_scheme.auth_mon_id
+_pdbx_nonpoly_scheme.pdb_strand_id
+_pdbx_nonpoly_scheme.pdb_ins_code
+C  2  HOH   1   25   25  HOH  HOH  A  .  
+C  2  HOH   2   31   31  HOH  HOH  A  .  
+C  2  HOH   3   32   32  HOH  HOH  A  .  
+C  2  HOH   4   36   36  HOH  HOH  A  .  
+C  2  HOH   5   38   38  HOH  HOH  A  .  
+C  2  HOH   6   39   39  HOH  HOH  A  .  
+C  2  HOH   7   40   40  HOH  HOH  A  .  
+C  2  HOH   8   41   41  HOH  HOH  A  .  
+C  2  HOH   9   50   50  HOH  HOH  A  .  
+C  2  HOH  10   54   54  HOH  HOH  A  .  
+C  2  HOH  11   55   55  HOH  HOH  A  .  
+C  2  HOH  12   58   58  HOH  HOH  A  .  
+C  2  HOH  13   62   62  HOH  HOH  A  .  
+C  2  HOH  14   64   64  HOH  HOH  A  .  
+C  2  HOH  15   65   65  HOH  HOH  A  .  
+C  2  HOH  16   66   66  HOH  HOH  A  .  
+C  2  HOH  17   67   67  HOH  HOH  A  .  
+C  2  HOH  18   70   70  HOH  HOH  A  .  
+C  2  HOH  19   74   74  HOH  HOH  A  .  
+C  2  HOH  20   76   76  HOH  HOH  A  .  
+C  2  HOH  21   77   77  HOH  HOH  A  .  
+C  2  HOH  22   78   78  HOH  HOH  A  .  
+C  2  HOH  23   79   79  HOH  HOH  A  .  
+C  2  HOH  24   80   80  HOH  HOH  A  .  
+C  2  HOH  25   81   81  HOH  HOH  A  .  
+C  2  HOH  26   82   82  HOH  HOH  A  .  
+C  2  HOH  27   84   84  HOH  HOH  A  .  
+C  2  HOH  28   85   85  HOH  HOH  A  .  
+C  2  HOH  29   86   86  HOH  HOH  A  .  
+C  2  HOH  30   87   87  HOH  HOH  A  .  
+C  2  HOH  31   88   88  HOH  HOH  A  .  
+C  2  HOH  32   89   89  HOH  HOH  A  .  
+C  2  HOH  33   92   92  HOH  HOH  A  .  
+C  2  HOH  34   97   97  HOH  HOH  A  .  
+C  2  HOH  35   99   99  HOH  HOH  A  .  
+C  2  HOH  36  100  100  HOH  HOH  A  .  
+C  2  HOH  37  102  102  HOH  HOH  A  .  
+D  2  HOH   1   26   26  HOH  HOH  B  .  
+D  2  HOH   2   27   27  HOH  HOH  B  .  
+D  2  HOH   3   28   28  HOH  HOH  B  .  
+D  2  HOH   4   29   29  HOH  HOH  B  .  
+D  2  HOH   5   30   30  HOH  HOH  B  .  
+D  2  HOH   6   33   33  HOH  HOH  B  .  
+D  2  HOH   7   34   34  HOH  HOH  B  .  
+D  2  HOH   8   35   35  HOH  HOH  B  .  
+D  2  HOH   9   37   37  HOH  HOH  B  .  
+D  2  HOH  10   42   42  HOH  HOH  B  .  
+D  2  HOH  11   43   43  HOH  HOH  B  .  
+D  2  HOH  12   44   44  HOH  HOH  B  .  
+D  2  HOH  13   45   45  HOH  HOH  B  .  
+D  2  HOH  14   46   46  HOH  HOH  B  .  
+D  2  HOH  15   47   47  HOH  HOH  B  .  
+D  2  HOH  16   48   48  HOH  HOH  B  .  
+D  2  HOH  17   49   49  HOH  HOH  B  .  
+D  2  HOH  18   51   51  HOH  HOH  B  .  
+D  2  HOH  19   52   52  HOH  HOH  B  .  
+D  2  HOH  20   53   53  HOH  HOH  B  .  
+D  2  HOH  21   56   56  HOH  HOH  B  .  
+D  2  HOH  22   57   57  HOH  HOH  B  .  
+D  2  HOH  23   59   59  HOH  HOH  B  .  
+D  2  HOH  24   60   60  HOH  HOH  B  .  
+D  2  HOH  25   61   61  HOH  HOH  B  .  
+D  2  HOH  26   63   63  HOH  HOH  B  .  
+D  2  HOH  27   68   68  HOH  HOH  B  .  
+D  2  HOH  28   69   69  HOH  HOH  B  .  
+D  2  HOH  29   71   71  HOH  HOH  B  .  
+D  2  HOH  30   72   72  HOH  HOH  B  .  
+D  2  HOH  31   73   73  HOH  HOH  B  .  
+D  2  HOH  32   75   75  HOH  HOH  B  .  
+D  2  HOH  33   83   83  HOH  HOH  B  .  
+D  2  HOH  34   90   90  HOH  HOH  B  .  
+D  2  HOH  35   91   91  HOH  HOH  B  .  
+D  2  HOH  36   93   93  HOH  HOH  B  .  
+D  2  HOH  37   94   94  HOH  HOH  B  .  
+D  2  HOH  38   95   95  HOH  HOH  B  .  
+D  2  HOH  39   96   96  HOH  HOH  B  .  
+D  2  HOH  40   98   98  HOH  HOH  B  .  
+D  2  HOH  41  101  101  HOH  HOH  B  .  
+D  2  HOH  42  103  103  HOH  HOH  B  .  
+D  2  HOH  43  104  104  HOH  HOH  B  .  
+##
+_pdbx_struct_assembly.id                  1
+_pdbx_struct_assembly.details             author_defined_assembly
+_pdbx_struct_assembly.method_details      ?
+_pdbx_struct_assembly.oligomeric_details  dimeric
+_pdbx_struct_assembly.oligomeric_count    2
+##
+_pdbx_struct_assembly_gen.assembly_id      1
+_pdbx_struct_assembly_gen.oper_expression  1
+_pdbx_struct_assembly_gen.asym_id_list     A,B,C,D
+##
+_pdbx_struct_oper_list.id                  1
+_pdbx_struct_oper_list.type                'identity operation'
+_pdbx_struct_oper_list.name                1_555
+_pdbx_struct_oper_list.symmetry_operation  x,y,z
+_pdbx_struct_oper_list.matrix[1][1]        1.0000000000
+_pdbx_struct_oper_list.matrix[1][2]        0.0000000000
+_pdbx_struct_oper_list.matrix[1][3]        0.0000000000
+_pdbx_struct_oper_list.vector[1]           0.0000000000
+_pdbx_struct_oper_list.matrix[2][1]        0.0000000000
+_pdbx_struct_oper_list.matrix[2][2]        1.0000000000
+_pdbx_struct_oper_list.matrix[2][3]        0.0000000000
+_pdbx_struct_oper_list.vector[2]           0.0000000000
+_pdbx_struct_oper_list.matrix[3][1]        0.0000000000
+_pdbx_struct_oper_list.matrix[3][2]        0.0000000000
+_pdbx_struct_oper_list.matrix[3][3]        1.0000000000
+_pdbx_struct_oper_list.vector[3]           0.0000000000
+##
+loop_
+_pdbx_audit_revision_history.ordinal
+_pdbx_audit_revision_history.data_content_type
+_pdbx_audit_revision_history.major_revision
+_pdbx_audit_revision_history.minor_revision
+_pdbx_audit_revision_history.revision_date
+1  'Structure model'  1  0  1981-05-21  
+2  'Structure model'  1  1  2008-05-22  
+3  'Structure model'  1  2  2011-07-13  
+##
+_pdbx_audit_revision_details.ordinal            1
+_pdbx_audit_revision_details.revision_ordinal   1
+_pdbx_audit_revision_details.data_content_type  'Structure model'
+_pdbx_audit_revision_details.provider           repository
+_pdbx_audit_revision_details.type               'Initial release'
+_pdbx_audit_revision_details.description        ?
+##
+loop_
+_pdbx_audit_revision_group.ordinal
+_pdbx_audit_revision_group.revision_ordinal
+_pdbx_audit_revision_group.data_content_type
+_pdbx_audit_revision_group.group
+1  2  'Structure model'  'Version format compliance'  
+2  3  'Structure model'  'Version format compliance'  
+##
+loop_
+_refine_B_iso.class
+_refine_B_iso.details
+_refine_B_iso.treatment
+_refine_B_iso.pdbx_refine_id
+'ALL ATOMS'   TR  isotropic  'X-RAY DIFFRACTION'  
+'ALL WATERS'  TR  isotropic  'X-RAY DIFFRACTION'  
+##
+loop_
+_refine_occupancy.class
+_refine_occupancy.treatment
+_refine_occupancy.pdbx_refine_id
+'ALL ATOMS'   fix  'X-RAY DIFFRACTION'  
+'ALL WATERS'  fix  'X-RAY DIFFRACTION'  
+##
+_software.name            JACK-LEVITT
+_software.classification  refinement
+_software.version         .
+_software.citation_id     ?
+_software.pdbx_ordinal    1
+##
+loop_
+_pdbx_validate_close_contact.id
+_pdbx_validate_close_contact.PDB_model_num
+_pdbx_validate_close_contact.auth_atom_id_1
+_pdbx_validate_close_contact.auth_asym_id_1
+_pdbx_validate_close_contact.auth_comp_id_1
+_pdbx_validate_close_contact.auth_seq_id_1
+_pdbx_validate_close_contact.PDB_ins_code_1
+_pdbx_validate_close_contact.label_alt_id_1
+_pdbx_validate_close_contact.auth_atom_id_2
+_pdbx_validate_close_contact.auth_asym_id_2
+_pdbx_validate_close_contact.auth_comp_id_2
+_pdbx_validate_close_contact.auth_seq_id_2
+_pdbx_validate_close_contact.PDB_ins_code_2
+_pdbx_validate_close_contact.label_alt_id_2
+_pdbx_validate_close_contact.dist
+1  1  O    A  HOH  62  ?  ?  O  A  HOH  77  ?  ?  1.61  
+2  1  OP2  A  DA    6  ?  ?  O  A  HOH  65  ?  ?  1.89  
+3  1  OP2  A  DG   10  ?  ?  O  A  HOH  70  ?  ?  2.02  
+4  1  O    A  HOH  54  ?  ?  O  A  HOH  86  ?  ?  2.07  
+5  1  O    A  HOH  77  ?  ?  O  B  HOH  63  ?  ?  2.09  
+6  1  O    A  HOH  31  ?  ?  O  A  HOH  99  ?  ?  2.12  
+##
+loop_
+_pdbx_validate_rmsd_bond.id
+_pdbx_validate_rmsd_bond.PDB_model_num
+_pdbx_validate_rmsd_bond.auth_atom_id_1
+_pdbx_validate_rmsd_bond.auth_asym_id_1
+_pdbx_validate_rmsd_bond.auth_comp_id_1
+_pdbx_validate_rmsd_bond.auth_seq_id_1
+_pdbx_validate_rmsd_bond.PDB_ins_code_1
+_pdbx_validate_rmsd_bond.label_alt_id_1
+_pdbx_validate_rmsd_bond.auth_atom_id_2
+_pdbx_validate_rmsd_bond.auth_asym_id_2
+_pdbx_validate_rmsd_bond.auth_comp_id_2
+_pdbx_validate_rmsd_bond.auth_seq_id_2
+_pdbx_validate_rmsd_bond.PDB_ins_code_2
+_pdbx_validate_rmsd_bond.label_alt_id_2
+_pdbx_validate_rmsd_bond.bond_value
+_pdbx_validate_rmsd_bond.bond_target_value
+_pdbx_validate_rmsd_bond.bond_deviation
+_pdbx_validate_rmsd_bond.bond_standard_deviation
+_pdbx_validate_rmsd_bond.linker_flag
+ 1  1  C5  A  DC   1  ?  ?  C6  A  DC   1  ?  ?  1.390  1.339   0.051  0.008  N  
+ 2  1  C5  A  DG   2  ?  ?  N7  A  DG   2  ?  ?  1.348  1.388  -0.040  0.006  N  
+ 3  1  N7  A  DG   2  ?  ?  C8  A  DG   2  ?  ?  1.341  1.305   0.036  0.006  N  
+ 4  1  C5  A  DC   3  ?  ?  C6  A  DC   3  ?  ?  1.391  1.339   0.052  0.008  N  
+ 5  1  C5  A  DG   4  ?  ?  N7  A  DG   4  ?  ?  1.349  1.388  -0.039  0.006  N  
+ 6  1  N7  A  DG   4  ?  ?  C8  A  DG   4  ?  ?  1.341  1.305   0.036  0.006  N  
+ 7  1  C5  A  DA   5  ?  ?  N7  A  DA   5  ?  ?  1.349  1.388  -0.039  0.006  N  
+ 8  1  C5  A  DA   6  ?  ?  N7  A  DA   6  ?  ?  1.350  1.388  -0.038  0.006  N  
+ 9  1  C5  A  DT   7  ?  ?  C6  A  DT   7  ?  ?  1.395  1.339   0.056  0.007  N  
+10  1  C5  A  DT   8  ?  ?  C6  A  DT   8  ?  ?  1.395  1.339   0.056  0.007  N  
+11  1  C5  A  DC   9  ?  ?  C6  A  DC   9  ?  ?  1.392  1.339   0.053  0.008  N  
+12  1  C5  A  DG  10  ?  ?  N7  A  DG  10  ?  ?  1.349  1.388  -0.039  0.006  N  
+13  1  N7  A  DG  10  ?  ?  C8  A  DG  10  ?  ?  1.341  1.305   0.036  0.006  N  
+14  1  C5  A  DC  11  ?  ?  C6  A  DC  11  ?  ?  1.390  1.339   0.051  0.008  N  
+15  1  C5  A  DG  12  ?  ?  N7  A  DG  12  ?  ?  1.349  1.388  -0.039  0.006  N  
+16  1  C5  B  DC  13  ?  ?  C6  B  DC  13  ?  ?  1.391  1.339   0.052  0.008  N  
+17  1  C5  B  DG  14  ?  ?  N7  B  DG  14  ?  ?  1.346  1.388  -0.042  0.006  N  
+18  1  C5  B  DC  15  ?  ?  C6  B  DC  15  ?  ?  1.388  1.339   0.049  0.008  N  
+19  1  C5  B  DG  16  ?  ?  N7  B  DG  16  ?  ?  1.348  1.388  -0.040  0.006  N  
+20  1  N7  B  DG  16  ?  ?  C8  B  DG  16  ?  ?  1.344  1.305   0.039  0.006  N  
+21  1  C5  B  DA  17  ?  ?  N7  B  DA  17  ?  ?  1.349  1.388  -0.039  0.006  N  
+22  1  C5  B  DA  18  ?  ?  N7  B  DA  18  ?  ?  1.349  1.388  -0.039  0.006  N  
+23  1  C5  B  DT  19  ?  ?  C6  B  DT  19  ?  ?  1.395  1.339   0.056  0.007  N  
+24  1  C5  B  DT  20  ?  ?  C6  B  DT  20  ?  ?  1.390  1.339   0.051  0.007  N  
+25  1  C5  B  DC  21  ?  ?  C6  B  DC  21  ?  ?  1.391  1.339   0.052  0.008  N  
+26  1  C5  B  DG  22  ?  ?  N7  B  DG  22  ?  ?  1.352  1.388  -0.036  0.006  N  
+27  1  N7  B  DG  22  ?  ?  C8  B  DG  22  ?  ?  1.342  1.305   0.037  0.006  N  
+28  1  C5  B  DC  23  ?  ?  C6  B  DC  23  ?  ?  1.390  1.339   0.051  0.008  N  
+29  1  C5  B  DG  24  ?  ?  N7  B  DG  24  ?  ?  1.349  1.388  -0.039  0.006  N  
+##
+loop_
+_pdbx_validate_rmsd_angle.id
+_pdbx_validate_rmsd_angle.PDB_model_num
+_pdbx_validate_rmsd_angle.auth_atom_id_1
+_pdbx_validate_rmsd_angle.auth_asym_id_1
+_pdbx_validate_rmsd_angle.auth_comp_id_1
+_pdbx_validate_rmsd_angle.auth_seq_id_1
+_pdbx_validate_rmsd_angle.PDB_ins_code_1
+_pdbx_validate_rmsd_angle.label_alt_id_1
+_pdbx_validate_rmsd_angle.auth_atom_id_2
+_pdbx_validate_rmsd_angle.auth_asym_id_2
+_pdbx_validate_rmsd_angle.auth_comp_id_2
+_pdbx_validate_rmsd_angle.auth_seq_id_2
+_pdbx_validate_rmsd_angle.PDB_ins_code_2
+_pdbx_validate_rmsd_angle.label_alt_id_2
+_pdbx_validate_rmsd_angle.auth_atom_id_3
+_pdbx_validate_rmsd_angle.auth_asym_id_3
+_pdbx_validate_rmsd_angle.auth_comp_id_3
+_pdbx_validate_rmsd_angle.auth_seq_id_3
+_pdbx_validate_rmsd_angle.PDB_ins_code_3
+_pdbx_validate_rmsd_angle.label_alt_id_3
+_pdbx_validate_rmsd_angle.angle_value
+_pdbx_validate_rmsd_angle.angle_target_value
+_pdbx_validate_rmsd_angle.angle_deviation
+_pdbx_validate_rmsd_angle.angle_standard_deviation
+_pdbx_validate_rmsd_angle.linker_flag
+ 1  1  "C4'"  A  DC   1  ?  ?  "C3'"  A  DC   1  ?  ?  "C2'"  A  DC   1  ?  ?   97.42  102.20  -4.78  0.70  N  
+ 2  1  "C3'"  A  DC   1  ?  ?  "C2'"  A  DC   1  ?  ?  "C1'"  A  DC   1  ?  ?   96.43  102.40  -5.97  0.80  N  
+ 3  1  "O4'"  A  DC   1  ?  ?  "C1'"  A  DC   1  ?  ?  "C2'"  A  DC   1  ?  ?  100.54  105.90  -5.36  0.80  N  
+ 4  1  "O5'"  A  DG   2  ?  ?  "C5'"  A  DG   2  ?  ?  "C4'"  A  DG   2  ?  ?  102.57  109.40  -6.83  0.80  N  
+ 5  1  "O4'"  A  DG   2  ?  ?  "C1'"  A  DG   2  ?  ?  N9     A  DG   2  ?  ?  103.05  108.00  -4.95  0.70  N  
+ 6  1  "O4'"  A  DC   3  ?  ?  "C1'"  A  DC   3  ?  ?  N1     A  DC   3  ?  ?  102.64  108.00  -5.36  0.70  N  
+ 7  1  "C3'"  A  DG   4  ?  ?  "C2'"  A  DG   4  ?  ?  "C1'"  A  DG   4  ?  ?   97.18  102.40  -5.22  0.80  N  
+ 8  1  "O4'"  A  DA   5  ?  ?  "C1'"  A  DA   5  ?  ?  "C2'"  A  DA   5  ?  ?  101.02  105.90  -4.88  0.80  N  
+ 9  1  "O4'"  A  DA   5  ?  ?  "C1'"  A  DA   5  ?  ?  N9     A  DA   5  ?  ?  103.63  108.00  -4.37  0.70  N  
+10  1  "O4'"  A  DA   6  ?  ?  "C1'"  A  DA   6  ?  ?  "C2'"  A  DA   6  ?  ?   99.84  105.90  -6.06  0.80  N  
+11  1  "O5'"  A  DT   7  ?  ?  "C5'"  A  DT   7  ?  ?  "C4'"  A  DT   7  ?  ?  103.29  109.40  -6.11  0.80  N  
+12  1  N1     A  DT   7  ?  ?  C2     A  DT   7  ?  ?  N3     A  DT   7  ?  ?  118.31  114.60   3.71  0.60  N  
+13  1  C2     A  DT   7  ?  ?  N3     A  DT   7  ?  ?  C4     A  DT   7  ?  ?  122.84  127.20  -4.36  0.60  N  
+14  1  C5     A  DT   7  ?  ?  C6     A  DT   7  ?  ?  N1     A  DT   7  ?  ?  119.96  123.70  -3.74  0.60  N  
+15  1  "O4'"  A  DT   8  ?  ?  "C1'"  A  DT   8  ?  ?  "C2'"  A  DT   8  ?  ?  100.50  105.90  -5.40  0.80  N  
+16  1  N1     A  DT   8  ?  ?  C2     A  DT   8  ?  ?  N3     A  DT   8  ?  ?  118.74  114.60   4.14  0.60  N  
+17  1  C2     A  DT   8  ?  ?  N3     A  DT   8  ?  ?  C4     A  DT   8  ?  ?  122.27  127.20  -4.93  0.60  N  
+18  1  C5     A  DT   8  ?  ?  C6     A  DT   8  ?  ?  N1     A  DT   8  ?  ?  119.58  123.70  -4.12  0.60  N  
+19  1  "C3'"  A  DG  10  ?  ?  "C2'"  A  DG  10  ?  ?  "C1'"  A  DG  10  ?  ?   95.28  102.40  -7.12  0.80  N  
+20  1  "O4'"  A  DG  12  ?  ?  "C1'"  A  DG  12  ?  ?  "C2'"  A  DG  12  ?  ?  100.00  105.90  -5.90  0.80  N  
+21  1  "O4'"  B  DC  13  ?  ?  "C1'"  B  DC  13  ?  ?  N1     B  DC  13  ?  ?  103.43  108.00  -4.57  0.70  N  
+22  1  "O5'"  B  DG  14  ?  ?  "C5'"  B  DG  14  ?  ?  "C4'"  B  DG  14  ?  ?  103.60  109.40  -5.80  0.80  N  
+23  1  "O4'"  B  DG  14  ?  ?  "C1'"  B  DG  14  ?  ?  "C2'"  B  DG  14  ?  ?  100.71  105.90  -5.19  0.80  N  
+24  1  "O4'"  B  DG  14  ?  ?  "C1'"  B  DG  14  ?  ?  N9     B  DG  14  ?  ?  102.65  108.00  -5.35  0.70  N  
+25  1  "O5'"  B  DG  16  ?  ?  "C5'"  B  DG  16  ?  ?  "C4'"  B  DG  16  ?  ?  104.35  109.40  -5.05  0.80  N  
+26  1  "O4'"  B  DA  18  ?  ?  "C1'"  B  DA  18  ?  ?  N9     B  DA  18  ?  ?  103.63  108.00  -4.37  0.70  N  
+27  1  N1     B  DT  19  ?  ?  C2     B  DT  19  ?  ?  N3     B  DT  19  ?  ?  118.52  114.60   3.92  0.60  N  
+28  1  C2     B  DT  19  ?  ?  N3     B  DT  19  ?  ?  C4     B  DT  19  ?  ?  122.33  127.20  -4.87  0.60  N  
+29  1  C5     B  DT  19  ?  ?  C6     B  DT  19  ?  ?  N1     B  DT  19  ?  ?  119.05  123.70  -4.65  0.60  N  
+30  1  "O4'"  B  DT  20  ?  ?  "C1'"  B  DT  20  ?  ?  "C2'"  B  DT  20  ?  ?   99.84  105.90  -6.06  0.80  N  
+31  1  "O4'"  B  DT  20  ?  ?  "C1'"  B  DT  20  ?  ?  N1     B  DT  20  ?  ?  102.84  108.00  -5.16  0.70  N  
+32  1  N1     B  DT  20  ?  ?  C2     B  DT  20  ?  ?  N3     B  DT  20  ?  ?  118.22  114.60   3.62  0.60  N  
+33  1  C2     B  DT  20  ?  ?  N3     B  DT  20  ?  ?  C4     B  DT  20  ?  ?  122.77  127.20  -4.43  0.60  N  
+34  1  C5     B  DT  20  ?  ?  C6     B  DT  20  ?  ?  N1     B  DT  20  ?  ?  119.69  123.70  -4.01  0.60  N  
+35  1  C6     B  DT  20  ?  ?  C5     B  DT  20  ?  ?  C7     B  DT  20  ?  ?  119.28  122.90  -3.62  0.60  N  
+36  1  "O4'"  B  DC  21  ?  ?  "C1'"  B  DC  21  ?  ?  N1     B  DC  21  ?  ?  100.91  108.00  -7.09  0.70  N  
+37  1  "C3'"  B  DG  22  ?  ?  "C2'"  B  DG  22  ?  ?  "C1'"  B  DG  22  ?  ?   95.55  102.40  -6.85  0.80  N  
+38  1  "O4'"  B  DG  22  ?  ?  "C1'"  B  DG  22  ?  ?  N9     B  DG  22  ?  ?  110.17  108.30   1.87  0.30  N  
+39  1  "O5'"  B  DG  24  ?  ?  "C5'"  B  DG  24  ?  ?  "C4'"  B  DG  24  ?  ?  103.92  109.40  -5.48  0.80  N  
+##
+_ndb_struct_conf_na.entry_id  1BNA
+_ndb_struct_conf_na.feature   'b-form double helix'
+##
+loop_
+_ndb_struct_na_base_pair.model_number
+_ndb_struct_na_base_pair.i_label_asym_id
+_ndb_struct_na_base_pair.i_label_comp_id
+_ndb_struct_na_base_pair.i_label_seq_id
+_ndb_struct_na_base_pair.i_symmetry
+_ndb_struct_na_base_pair.j_label_asym_id
+_ndb_struct_na_base_pair.j_label_comp_id
+_ndb_struct_na_base_pair.j_label_seq_id
+_ndb_struct_na_base_pair.j_symmetry
+_ndb_struct_na_base_pair.shear
+_ndb_struct_na_base_pair.stretch
+_ndb_struct_na_base_pair.stagger
+_ndb_struct_na_base_pair.buckle
+_ndb_struct_na_base_pair.propeller
+_ndb_struct_na_base_pair.opening
+_ndb_struct_na_base_pair.pair_number
+_ndb_struct_na_base_pair.pair_name
+_ndb_struct_na_base_pair.i_auth_asym_id
+_ndb_struct_na_base_pair.i_auth_seq_id
+_ndb_struct_na_base_pair.i_PDB_ins_code
+_ndb_struct_na_base_pair.j_auth_asym_id
+_ndb_struct_na_base_pair.j_auth_seq_id
+_ndb_struct_na_base_pair.j_PDB_ins_code
+_ndb_struct_na_base_pair.hbond_type_28
+_ndb_struct_na_base_pair.hbond_type_12
+1  A  DC   1  1_555  B  DG  12  1_555  -0.422  -0.268   0.060    2.762  -14.200  -3.666   1  A_DC1:DG24_B   A   1  ?  B  24  ?  19  1  
+1  A  DG   2  1_555  B  DC  11  1_555  -0.024  -0.266   0.249   -4.455  -10.846  -4.022   2  A_DG2:DC23_B   A   2  ?  B  23  ?  19  1  
+1  A  DC   3  1_555  B  DG  10  1_555   0.003  -0.248   0.213   -6.940   -3.928  -2.346   3  A_DC3:DG22_B   A   3  ?  B  22  ?  19  1  
+1  A  DG   4  1_555  B  DC   9  1_555  -0.371  -0.442  -0.180    9.308  -10.394  -1.297   4  A_DG4:DC21_B   A   4  ?  B  21  ?  19  1  
+1  A  DA   5  1_555  B  DT   8  1_555   0.272  -0.222   0.035    5.035  -16.362   1.835   5  A_DA5:DT20_B   A   5  ?  B  20  ?  20  1  
+1  A  DA   6  1_555  B  DT   7  1_555  -0.092  -0.042   0.166    3.544  -18.130   5.558   6  A_DA6:DT19_B   A   6  ?  B  19  ?  20  1  
+1  A  DT   7  1_555  B  DA   6  1_555   0.317  -0.117   0.133    0.829  -17.701   7.931   7  A_DT7:DA18_B   A   7  ?  B  18  ?  20  1  
+1  A  DT   8  1_555  B  DA   5  1_555   0.249  -0.215  -0.099   -1.329  -17.674   0.828   8  A_DT8:DA17_B   A   8  ?  B  17  ?  20  1  
+1  A  DC   9  1_555  B  DG   4  1_555  -0.019  -0.251  -0.060  -10.176  -17.254  -0.867   9  A_DC9:DG16_B   A   9  ?  B  16  ?  19  1  
+1  A  DG  10  1_555  B  DC   3  1_555   0.087  -0.278   0.272    1.665   -5.307  -1.129  10  A_DG10:DC15_B  A  10  ?  B  15  ?  19  1  
+1  A  DC  11  1_555  B  DG   2  1_555   0.069  -0.284   0.586   -3.958  -18.046  -5.616  11  A_DC11:DG14_B  A  11  ?  B  14  ?  19  1  
+1  A  DG  12  1_555  B  DC   1  1_555  -0.529  -0.109   0.261    6.598    1.957  -3.864  12  A_DG12:DC13_B  A  12  ?  B  13  ?  19  1  
+##
+loop_
+_ndb_struct_na_base_pair_step.model_number
+_ndb_struct_na_base_pair_step.i_label_asym_id_1
+_ndb_struct_na_base_pair_step.i_label_comp_id_1
+_ndb_struct_na_base_pair_step.i_label_seq_id_1
+_ndb_struct_na_base_pair_step.i_symmetry_1
+_ndb_struct_na_base_pair_step.j_label_asym_id_1
+_ndb_struct_na_base_pair_step.j_label_comp_id_1
+_ndb_struct_na_base_pair_step.j_label_seq_id_1
+_ndb_struct_na_base_pair_step.j_symmetry_1
+_ndb_struct_na_base_pair_step.i_label_asym_id_2
+_ndb_struct_na_base_pair_step.i_label_comp_id_2
+_ndb_struct_na_base_pair_step.i_label_seq_id_2
+_ndb_struct_na_base_pair_step.i_symmetry_2
+_ndb_struct_na_base_pair_step.j_label_asym_id_2
+_ndb_struct_na_base_pair_step.j_label_comp_id_2
+_ndb_struct_na_base_pair_step.j_label_seq_id_2
+_ndb_struct_na_base_pair_step.j_symmetry_2
+_ndb_struct_na_base_pair_step.shift
+_ndb_struct_na_base_pair_step.slide
+_ndb_struct_na_base_pair_step.rise
+_ndb_struct_na_base_pair_step.tilt
+_ndb_struct_na_base_pair_step.roll
+_ndb_struct_na_base_pair_step.twist
+_ndb_struct_na_base_pair_step.x_displacement
+_ndb_struct_na_base_pair_step.y_displacement
+_ndb_struct_na_base_pair_step.helical_rise
+_ndb_struct_na_base_pair_step.inclination
+_ndb_struct_na_base_pair_step.tip
+_ndb_struct_na_base_pair_step.helical_twist
+_ndb_struct_na_base_pair_step.step_number
+_ndb_struct_na_base_pair_step.step_name
+_ndb_struct_na_base_pair_step.i_auth_asym_id_1
+_ndb_struct_na_base_pair_step.i_auth_seq_id_1
+_ndb_struct_na_base_pair_step.i_PDB_ins_code_1
+_ndb_struct_na_base_pair_step.j_auth_asym_id_1
+_ndb_struct_na_base_pair_step.j_auth_seq_id_1
+_ndb_struct_na_base_pair_step.j_PDB_ins_code_1
+_ndb_struct_na_base_pair_step.i_auth_asym_id_2
+_ndb_struct_na_base_pair_step.i_auth_seq_id_2
+_ndb_struct_na_base_pair_step.i_PDB_ins_code_2
+_ndb_struct_na_base_pair_step.j_auth_asym_id_2
+_ndb_struct_na_base_pair_step.j_auth_seq_id_2
+_ndb_struct_na_base_pair_step.j_PDB_ins_code_2
+1  A  DC   1  1_555  B  DG  12  1_555  A  DG   2  1_555  B  DC  11  1_555  -0.362   0.149  3.524  -3.397    6.425  40.311  -0.551   0.114  3.524    9.231   4.881  40.934   1  AA_DC1DG2:DC23DG24_BB    A   1  ?  B  24  ?  A   2  ?  B  23  ?  
+1  A  DG   2  1_555  B  DC  11  1_555  A  DC   3  1_555  B  DG  10  1_555   0.498   0.227  3.523   0.805   -4.734  38.147   0.977  -0.648  3.480   -7.209  -1.225  38.437   2  AA_DG2DC3:DG22DC23_BB    A   2  ?  B  23  ?  A   3  ?  B  22  ?  
+1  A  DC   3  1_555  B  DG  10  1_555  A  DG   4  1_555  B  DC   9  1_555  -0.324   0.689  3.041   3.631    7.947  24.466  -0.563   1.680  3.033   18.033  -8.240  25.957   3  AA_DC3DG4:DC21DG22_BB    A   3  ?  B  22  ?  A   4  ?  B  21  ?  
+1  A  DG   4  1_555  B  DC   9  1_555  A  DA   5  1_555  B  DT   8  1_555   0.008   0.071  3.360  -2.678    3.162  40.897  -0.252  -0.310  3.349    4.511   3.821  41.097   4  AA_DG4DA5:DT20DC21_BB    A   4  ?  B  21  ?  A   5  ?  B  20  ?  
+1  A  DA   5  1_555  B  DT   8  1_555  A  DA   6  1_555  B  DT   7  1_555   0.101  -0.312  3.318  -0.705    0.950  35.351  -0.655  -0.272  3.306    1.564   1.160  35.370   5  AA_DA5DA6:DT19DT20_BB    A   5  ?  B  20  ?  A   6  ?  B  19  ?  
+1  A  DA   6  1_555  B  DT   7  1_555  A  DT   7  1_555  B  DA   6  1_555   0.329  -0.603  3.341   1.827   -2.755  34.760  -0.576  -0.264  3.390   -4.598  -3.049  34.912   6  AA_DA6DT7:DA18DT19_BB    A   6  ?  B  19  ?  A   7  ?  B  18  ?  
+1  A  DT   7  1_555  B  DA   6  1_555  A  DT   8  1_555  B  DA   5  1_555  -0.306  -0.175  3.318   2.964    0.725  35.393  -0.395   0.939  3.279    1.190  -4.864  35.520   7  AA_DT7DT8:DA17DA18_BB    A   7  ?  B  18  ?  A   8  ?  B  17  ?  
+1  A  DT   8  1_555  B  DA   5  1_555  A  DC   9  1_555  B  DG   4  1_555   0.020  -0.033  3.394   0.331   -0.053  39.272  -0.042   0.011  3.394   -0.079  -0.493  39.273   8  AA_DT8DC9:DG16DA17_BB    A   8  ?  B  17  ?  A   9  ?  B  16  ?  
+1  A  DC   9  1_555  B  DG   4  1_555  A  DG  10  1_555  B  DC   3  1_555   0.381   0.864  3.239  -3.294    3.860  29.397   0.874  -1.427  3.262    7.535   6.431  29.823   9  AA_DC9DG10:DC15DG16_BB   A   9  ?  B  16  ?  A  10  ?  B  15  ?  
+1  A  DG  10  1_555  B  DC   3  1_555  A  DC  11  1_555  B  DG   2  1_555  -1.303   0.418  3.682  -4.681  -12.201  40.779   1.959   1.257  3.543  -16.990   6.519  42.737  10  AA_DG10DC11:DG14DC15_BB  A  10  ?  B  15  ?  A  11  ?  B  14  ?  
+1  A  DC  11  1_555  B  DG   2  1_555  A  DG  12  1_555  B  DC   1  1_555   0.773   0.057  3.226   3.143   -3.090  32.624   0.626  -0.830  3.265   -5.469  -5.562  32.912  11  AA_DC11DG12:DC13DG14_BB  A  11  ?  B  14  ?  A  12  ?  B  13  ?  
+##
+_pdbx_entity_nonpoly.entity_id  2
+_pdbx_entity_nonpoly.name       water
+_pdbx_entity_nonpoly.comp_id    HOH
+##
+_ndb_struct_ntc_overall.entry_id                     1BNA
+_ndb_struct_ntc_overall.confal_score                 0
+_ndb_struct_ntc_overall.confal_percentile            0
+_ndb_struct_ntc_overall.ntc_version                  6.6.6
+_ndb_struct_ntc_overall.cana_version                 6.6.6
+_ndb_struct_ntc_overall.num_steps                    22
+_ndb_struct_ntc_overall.num_classified               22
+_ndb_struct_ntc_overall.num_unclassified             0
+_ndb_struct_ntc_overall.num_unclassified_rmsd_close  0
+##
+loop_
+_ndb_struct_ntc_step.id
+_ndb_struct_ntc_step.name
+_ndb_struct_ntc_step.PDB_model_number
+_ndb_struct_ntc_step.label_entity_id_1
+_ndb_struct_ntc_step.label_asym_id_1
+_ndb_struct_ntc_step.label_seq_id_1
+_ndb_struct_ntc_step.label_comp_id_1
+_ndb_struct_ntc_step.label_alt_id_1
+_ndb_struct_ntc_step.label_entity_id_2
+_ndb_struct_ntc_step.label_asym_id_2
+_ndb_struct_ntc_step.label_seq_id_2
+_ndb_struct_ntc_step.label_comp_id_2
+_ndb_struct_ntc_step.label_alt_id_2
+_ndb_struct_ntc_step.auth_asym_id_1
+_ndb_struct_ntc_step.auth_seq_id_1
+_ndb_struct_ntc_step.auth_asym_id_2
+_ndb_struct_ntc_step.auth_seq_id_2
+_ndb_struct_ntc_step.PDB_ins_code_1
+_ndb_struct_ntc_step.PDB_ins_code_2
+ 1    1bna_A_DC_1_DG_2  1  1  A   1  DC  .  1  A   2  DG  .  A   1  A   2  .  .  
+ 2    1bna_A_DG_2_DC_3  1  1  A   2  DG  .  1  A   3  DC  .  A   2  A   3  .  .  
+ 3    1bna_A_DC_3_DG_4  1  1  A   3  DC  .  1  A   4  DG  .  A   3  A   4  .  .  
+ 4    1bna_A_DG_4_DA_5  1  1  A   4  DG  .  1  A   5  DA  .  A   4  A   5  .  .  
+ 5    1bna_A_DA_5_DA_6  1  1  A   5  DA  .  1  A   6  DA  .  A   5  A   6  .  .  
+ 6    1bna_A_DA_6_DT_7  1  1  A   6  DA  .  1  A   7  DT  .  A   6  A   7  .  .  
+ 7    1bna_A_DT_7_DT_8  1  1  A   7  DT  .  1  A   8  DT  .  A   7  A   8  .  .  
+ 8    1bna_A_DT_8_DC_9  1  1  A   8  DT  .  1  A   9  DC  .  A   8  A   9  .  .  
+ 9   1bna_A_DC_9_DG_10  1  1  A   9  DC  .  1  A  10  DG  .  A   9  A  10  .  .  
+10  1bna_A_DG_10_DC_11  1  1  A  10  DG  .  1  A  11  DC  .  A  10  A  11  .  .  
+11  1bna_A_DC_11_DG_12  1  1  A  11  DC  .  1  A  12  DG  .  A  11  A  12  .  .  
+12  1bna_B_DC_13_DG_14  1  1  B   1  DC  .  1  B   2  DG  .  B  13  B  14  .  .  
+13  1bna_B_DG_14_DC_15  1  1  B   2  DG  .  1  B   3  DC  .  B  14  B  15  .  .  
+14  1bna_B_DC_15_DG_16  1  1  B   3  DC  .  1  B   4  DG  .  B  15  B  16  .  .  
+15  1bna_B_DG_16_DA_17  1  1  B   4  DG  .  1  B   5  DA  .  B  16  B  17  .  .  
+16  1bna_B_DA_17_DA_18  1  1  B   5  DA  .  1  B   6  DA  .  B  17  B  18  .  .  
+17  1bna_B_DA_18_DT_19  1  1  B   6  DA  .  1  B   7  DT  .  B  18  B  19  .  .  
+18  1bna_B_DT_19_DT_20  1  1  B   7  DT  .  1  B   8  DT  .  B  19  B  20  .  .  
+19  1bna_B_DT_20_DC_21  1  1  B   8  DT  .  1  B   9  DC  .  B  20  B  21  .  .  
+20  1bna_B_DC_21_DG_22  1  1  B   9  DC  .  1  B  10  DG  .  B  21  B  22  .  .  
+21  1bna_B_DG_22_DC_23  1  1  B  10  DG  .  1  B  11  DC  .  B  22  B  23  .  .  
+22  1bna_B_DC_23_DG_24  1  1  B  11  DC  .  1  B  12  DG  .  B  23  B  24  .  .  
+##
+loop_
+_ndb_struct_ntc_step_summary.step_id
+_ndb_struct_ntc_step_summary.assigned_CANA
+_ndb_struct_ntc_step_summary.assigned_NtC
+_ndb_struct_ntc_step_summary.confal_score
+_ndb_struct_ntc_step_summary.euclidean_distance_NtC_ideal
+_ndb_struct_ntc_step_summary.cartesian_rmsd_closest_NtC_representative
+_ndb_struct_ntc_step_summary.closest_CANA
+_ndb_struct_ntc_step_summary.closest_NtC
+_ndb_struct_ntc_step_summary.closest_step_golden
+ 1  B12  BB04  33  40.0  0.367  B12  BB04      1mjo_F_DG_10_DA_11  
+ 2  B-A  BA05  80  21.0  0.310  B-A  BA05      4i2o_W_DA_11_DT_12  
+ 3  A-B  AB01  36  25.1  0.177  A-B  AB01    1n1o_B_DC_115_DG_116  
+ 4  B12  BB04  16  39.9  0.216  B12  BB04    1kx5_J_DG_-56_DA_-55  
+ 5  BBB  BB01  68  30.9  0.190  BBB  BB01    3opi_B_DT_219_DT_220  
+ 6  B-A  BA05  69  25.9  0.274  B-A  BA05        1g8n_A_DA_6_DT_7  
+ 7  BBB  BB01  41  43.1  0.284  BBB  BB01    3opi_A_DT_107_DT_108  
+ 8  BBB  BB01  67  32.4  0.248  BBB  BB01      4f3u_B_DT_19_DT_20  
+ 9  BBB  BB00  77  33.4  0.373  BBB  BB00       1edr_A_DC_9_DG_10  
+10  BB2  BB07  70  37.3  0.387  BB2  BB07    1s32_J_DC_193_DT_194  
+11  BBB  BB01  54  36.1  0.382  BBB  BB01        1aay_B_DG_4_DT_5  
+12  BBB  BB00  48  49.0  0.277  BBB  BB00       1eo4_C_DC_9_DT_10  
+13  B-A  BA05  59  29.7  0.288  B-A  BA05      1g8n_B_DG_14_DC_15  
+14  A-B  AB01  63  32.0  0.260  A-B  AB01  1dsz_D_DC_1543_DT_1544  
+15  BBB  BB00  84  23.0  0.229  BBB  BB00    1p3l_J_DT_237_DT_238  
+16  BBB  BB00  88  18.5  0.133  BBB  BB00      1mus_B_DA_15_DG_16  
+17  BBB  BB01  64  24.6  0.197  BBB  BB01        463d_A_DA_6_DT_7  
+18  BBB  BB01  67  32.2  0.183  BBB  BB01        4mkw_A_DT_7_DT_8  
+19  BBB  BB01  78  19.4  0.144  BBB  BB01        1g75_A_DA_5_DA_6  
+20  BBB  BB00  16  39.3  0.271  BBB  BB00      1dpn_B_DC_21_DG_22  
+21  B-A  BA17  91  21.2  0.185  B-A  BA17      3utb_J_DC_-5_DT_-4  
+22  B-A  BA05  38  40.9  0.373  B-A  BA05    1qna_F_DC_218_DC_219  
+##
+loop_
+_ndb_struct_ntc_step_parameters.step_id
+_ndb_struct_ntc_step_parameters.tor_delta_1
+_ndb_struct_ntc_step_parameters.tor_epsilon_1
+_ndb_struct_ntc_step_parameters.tor_zeta_1
+_ndb_struct_ntc_step_parameters.tor_alpha_2
+_ndb_struct_ntc_step_parameters.tor_beta_2
+_ndb_struct_ntc_step_parameters.tor_gamma_2
+_ndb_struct_ntc_step_parameters.tor_delta_2
+_ndb_struct_ntc_step_parameters.tor_chi_1
+_ndb_struct_ntc_step_parameters.tor_chi_2
+_ndb_struct_ntc_step_parameters.dist_NN
+_ndb_struct_ntc_step_parameters.dist_CC
+_ndb_struct_ntc_step_parameters.tor_NCCN
+_ndb_struct_ntc_step_parameters.diff_tor_delta_1
+_ndb_struct_ntc_step_parameters.diff_tor_epsilon_1
+_ndb_struct_ntc_step_parameters.diff_tor_zeta_1
+_ndb_struct_ntc_step_parameters.diff_tor_alpha_2
+_ndb_struct_ntc_step_parameters.diff_tor_beta_2
+_ndb_struct_ntc_step_parameters.diff_tor_gamma_2
+_ndb_struct_ntc_step_parameters.diff_tor_delta_2
+_ndb_struct_ntc_step_parameters.diff_tor_chi_1
+_ndb_struct_ntc_step_parameters.diff_tor_chi_2
+_ndb_struct_ntc_step_parameters.diff_dist_NN
+_ndb_struct_ntc_step_parameters.diff_dist_CC
+_ndb_struct_ntc_step_parameters.diff_tor_NCCN
+_ndb_struct_ntc_step_parameters.confal_tor_delta_1
+_ndb_struct_ntc_step_parameters.confal_tor_epsilon_1
+_ndb_struct_ntc_step_parameters.confal_tor_zeta_1
+_ndb_struct_ntc_step_parameters.confal_tor_alpha_2
+_ndb_struct_ntc_step_parameters.confal_tor_beta_2
+_ndb_struct_ntc_step_parameters.confal_tor_gamma_2
+_ndb_struct_ntc_step_parameters.confal_tor_delta_2
+_ndb_struct_ntc_step_parameters.confal_tor_chi_1
+_ndb_struct_ntc_step_parameters.confal_tor_chi_2
+_ndb_struct_ntc_step_parameters.confal_dist_NN
+_ndb_struct_ntc_step_parameters.confal_dist_CC
+_ndb_struct_ntc_step_parameters.confal_tor_NCCN
+_ndb_struct_ntc_step_parameters.details
+ 1  156.8  218.7  216.1  294.4  169.8  40.1  128.1  255.0  249.5  4.65  5.35  32.3   16.7   17.4    1.9  -20.4   17.2  -6.0  -11.9   -7.5   -3.1   0.00   0.23   3.7    5.5   54.2   99.2   31.0   47.8   85.4  26.1   87.5  97.7  100.0  80.6  94.5  .  
+ 2  128.1  174.2  262.2  297.4  171.8  58.8   98.3  249.5  224.9  4.23  4.72  31.8   -3.3  -10.2   -6.5    1.5    3.0   6.5   -5.9   -1.4  -10.5   0.13   0.08   7.6   97.9   55.2   75.3   99.4   92.9   84.2  87.5   99.4  74.3   74.0  88.4  58.9  .  
+ 3   98.3  183.3  272.4  297.1  180.1  57.2  155.7  224.9  266.6  4.57  5.12  22.2   12.1   -2.9   -8.8   -3.9    1.6   2.8   13.8    2.1   10.6  -0.12  -0.16   4.5    8.9   96.9   77.0   94.8   99.1   96.7   8.7   98.8  70.1   93.1  90.4  89.4  .  
+ 4  155.7  204.7  207.5  317.0  142.8  52.4  119.6  266.6  233.7  4.25  4.73  35.8   15.6    3.4   -6.7    2.2   -9.9   6.3  -20.4    4.1  -18.8  -0.39  -0.39   7.2    8.0   97.7   90.4   98.6   78.3   83.7   1.9   96.2  42.1   58.4  53.1  80.6  .  
+ 5  119.6  179.9  267.8  286.7  179.7  66.0  121.1  233.7  237.8  4.47  5.04  27.7  -11.1   -1.0    2.2  -14.7    3.5  17.4    1.0  -13.8   -6.0   0.17   0.19   2.2   74.5   99.6   97.0   50.5   92.8   27.4  99.0   51.5  81.9   83.0  74.6  94.1  .  
+ 6  121.1  173.7  271.5  303.4  180.8  52.2   98.9  237.8  232.7  4.22  4.79  25.8  -10.3  -10.7    2.8    7.5   12.0  -0.1   -5.3  -13.0   -2.7   0.12   0.14   1.7   81.1   51.8   95.1   85.3   30.2  100.0  89.7   57.2  98.0   77.7  65.4  97.5  .  
+ 7   98.9  173.6  274.1  300.8  173.4  64.1  108.9  232.7  234.3  4.45  4.87  23.8  -31.8   -7.4    8.6   -0.6   -2.9  15.5  -11.3  -14.9   -9.6   0.15   0.02  -1.8    9.0   82.7   64.4   99.9   95.1   36.0  23.4   46.2  60.3   86.5  99.8  96.0  .  
+ 8  108.9  170.6  270.7  301.5  180.5  60.5  128.7  234.3  240.5  4.37  4.92  29.0  -21.9  -10.3    5.1    0.1    4.3  11.9    8.5  -13.2   -3.4   0.07   0.07   3.4   32.2   68.8   85.6  100.0   89.4   54.7  43.4   54.2  93.9   96.9  96.4  85.6  .  
+ 9  128.7  203.1  266.0  292.7  169.1  47.2  142.9  240.5  270.4  4.29  4.92  28.2   -9.1   20.0    7.8  -11.0  -10.5   3.0    4.7  -12.1   12.2  -0.09  -0.03   2.6   63.3   40.2   89.6   76.0   80.5   95.6  84.8   71.2  75.4   97.6  99.8  96.3  .  
+10  142.9  256.7  150.2  286.1  139.3  56.3  135.7  270.4  234.9  5.12  5.26  44.3   -0.8    9.4  -19.3  -10.4   -1.7  10.2   -5.5   -0.3  -25.5   0.16   0.05  -1.7   99.4   83.3   39.6   74.7   99.0   58.0  83.5  100.0  31.7   90.7  98.5  99.5  .  
+11  135.7  198.2  270.4  278.5  175.7  57.2  110.7  234.9  248.0  4.42  5.12  24.0    4.9   17.2    4.9  -22.9   -0.6   8.5   -9.5  -12.6    4.1   0.12   0.27  -1.6   94.4   35.4   86.8   18.9   99.8   73.3  35.6   57.3  91.2   91.0  54.7  96.7  .  
+12  136.7  201.4  235.1  308.7  163.9  49.0  121.9  232.4  243.6  4.87  5.23  30.1   -1.1   18.2  -23.1    5.0  -15.7   4.8  -16.3  -20.2  -14.7   0.50   0.28   4.6   99.3   46.8   38.2   94.6   61.4   88.8  14.3   38.7  66.3   45.3  77.2  89.4  .  
+13  121.9  177.7  267.0  297.0  168.8  60.4   85.7  243.6  226.2  4.07  4.51  37.5   -9.5   -6.7   -1.8    1.1    0.0   8.1  -18.6   -7.3   -9.2  -0.04  -0.13  13.3   83.6   77.5   97.9   99.6  100.0   76.6  26.6   84.0  79.9   97.0  69.8  19.9  .  
+14   85.7  174.8  274.5  290.8  171.1  73.2  135.9  226.2  245.2  4.56  4.99  26.2   -0.6  -11.4   -6.7  -10.2   -7.5  18.8   -5.9    3.5  -10.7  -0.13  -0.29   8.4   99.4   61.8   85.9   68.6   81.8   21.3  64.1   96.7  69.8   92.2  72.2  67.5  .  
+15  135.9  174.1  261.6  303.4  190.5  53.8  146.6  245.2  253.6  4.45  5.04  33.3   -1.9   -9.0    3.4   -0.3   10.9   9.6    8.5   -7.3   -4.7   0.08   0.10   7.7   98.0   83.2   97.9  100.0   79.2   61.9  59.1   88.2  95.9   97.9  97.0  72.5  .  
+16  146.6  176.9  262.9  302.9  186.4  47.7  130.2  253.6  251.7  4.25  4.83  28.9    8.8   -6.2    4.7   -0.8    6.8   3.5   -7.9    1.0   -6.5  -0.12  -0.12   3.3   65.6   91.7   96.1   99.9   91.3   93.9  63.0   99.8  92.1   95.3  95.7  94.2  .  
+17  130.2  174.4  258.7  301.7  173.6  60.0  109.2  251.7  228.7  4.28  4.74  25.3   -0.5   -6.5   -6.8    0.3   -2.6  11.4  -10.9    4.2  -15.2  -0.02  -0.11  -0.3   99.9   86.1   75.6  100.0   95.9   57.3  25.3   94.1  28.3   99.8  90.2  99.9  .  
+18  109.2  178.8  271.7  301.4  179.5  55.3  122.4  228.7  239.5  4.52  5.06  23.1  -21.5   -2.2    6.2   -0.0    3.3   6.7    2.2  -18.8   -4.3   0.21   0.21  -2.4   33.2   98.4   79.7  100.0   93.7   82.5  94.4   29.1  90.2   73.9  69.4  92.6  .  
+19  122.4  178.5  265.5  300.9  184.6  45.0  110.3  239.5  245.7  4.41  5.04  27.7   -8.4   -2.4   -0.1   -0.5    8.4  -3.7   -9.8   -8.0    1.8   0.10   0.19   2.2   84.7   98.0  100.0   99.9   65.3   94.4  33.2   80.1  98.2   93.1  73.9  94.1  .  
+20  110.3  183.3  273.5  293.2  179.1  50.2  149.7  245.7  271.6  4.23  4.84  22.1  -27.5    0.2   15.4  -10.5   -0.4   6.0   11.6   -6.8   13.4  -0.15  -0.11  -3.4    1.6  100.0   65.3   78.1  100.0   83.1  37.3   89.7  71.1   93.2  96.5  93.9  .  
+21  149.7  259.9  171.6  287.8  138.5  44.6  112.8  271.6  234.7  4.65  5.00  34.3    0.4    6.6   -5.0   -7.1    7.8   0.4   15.0    0.4    2.1   0.11   0.10  -3.9  100.0   91.2   94.0   89.3   86.2   99.9  57.4  100.0  98.9   96.4  96.2  96.2  .  
+22  112.8  185.6  263.2  295.0  170.6  46.6   78.7  234.7  224.8  4.30  4.90  35.4  -18.6    1.3   -5.5   -0.9    1.8  -5.7  -25.5  -16.2  -10.6   0.19   0.25  11.2   50.3   99.1   81.7   99.8   97.5   87.7   8.2   42.1  74.2   51.2  26.3  31.8  .  
+##
+loop_
+_ndb_struct_sugar_step_parameters.step_id
+_ndb_struct_sugar_step_parameters.P_1
+_ndb_struct_sugar_step_parameters.tau_1
+_ndb_struct_sugar_step_parameters.Pn_1
+_ndb_struct_sugar_step_parameters.P_2
+_ndb_struct_sugar_step_parameters.tau_2
+_ndb_struct_sugar_step_parameters.Pn_2
+_ndb_struct_sugar_step_parameters.nu_1_1
+_ndb_struct_sugar_step_parameters.nu_1_2
+_ndb_struct_sugar_step_parameters.nu_1_3
+_ndb_struct_sugar_step_parameters.nu_1_4
+_ndb_struct_sugar_step_parameters.nu_1_5
+_ndb_struct_sugar_step_parameters.nu_2_1
+_ndb_struct_sugar_step_parameters.nu_2_2
+_ndb_struct_sugar_step_parameters.nu_2_3
+_ndb_struct_sugar_step_parameters.nu_2_4
+_ndb_struct_sugar_step_parameters.nu_2_5
+_ndb_struct_sugar_step_parameters.diff_nu_1_1
+_ndb_struct_sugar_step_parameters.diff_nu_1_2
+_ndb_struct_sugar_step_parameters.diff_nu_1_3
+_ndb_struct_sugar_step_parameters.diff_nu_1_4
+_ndb_struct_sugar_step_parameters.diff_nu_1_5
+_ndb_struct_sugar_step_parameters.diff_nu_2_1
+_ndb_struct_sugar_step_parameters.diff_nu_2_2
+_ndb_struct_sugar_step_parameters.diff_nu_2_3
+_ndb_struct_sugar_step_parameters.diff_nu_2_4
+_ndb_struct_sugar_step_parameters.diff_nu_2_5
+ 1  161.2  55.3  C2end  139.5  41.7  C1exo  326.0  54.5  307.6   33.7    0.0  323.9   41.9  328.3   10.8   16.1   -6.6  16.4  -18.5   14.8  -5.1  -12.8    6.9    1.1   -9.2   14.2  
+ 2  139.5  41.7  C1exo   92.8  38.3  O4end  323.9  41.9  328.3   10.8   16.1  322.4   23.3  358.1  339.7   36.6  -10.4   9.2   -4.3   -2.2   8.3    2.1   -5.3    5.9   -5.1    2.4  
+ 3   92.8  38.3  O4end  166.3  48.5  C2end  322.4  23.3  358.1  339.7   36.6  333.3   46.3  312.9   33.4  355.6  -17.8  25.5  -23.5   13.6   3.0   -5.9   12.9  -14.3   12.0   -3.9  
+ 4  166.3  48.5  C2end  128.6  46.3  C1exo  333.3  46.3  312.9   33.4  355.6  315.3   44.3  331.1    3.4   25.8    0.6   8.2  -13.3   14.5  -9.5  -21.4    9.3    3.9  -16.6   23.9  
+ 5  128.6  46.3  C1exo  126.9  49.8  C1exo  315.3  44.3  331.1    3.4   25.8  311.9   47.5  330.1    2.2   29.0  -19.6  12.1   -1.8   -9.6  18.4  -14.7   14.1   -8.5   -0.1    9.5  
+ 6  126.9  49.8  C1exo  101.3  47.3  O4end  311.9  47.5  330.1    2.2   29.0  311.9   34.1  350.7  341.5   42.1  -22.4  14.9   -2.5  -10.8  21.1   -8.4    5.5   -1.5   -3.4    7.8  
+ 7  101.3  47.3  O4end  115.6  49.2  C1exo  311.9  34.1  350.7  341.5   42.1  309.9   42.8  338.7  352.6   36.2  -23.0   2.0   17.8  -31.5  34.6  -16.7    9.4    0.1   -9.7   16.8  
+ 8  115.6  49.2  C1exo  140.4  46.4  C1exo  309.9  42.8  338.7  352.6   36.2  319.6   46.3  324.3   12.9   17.1  -25.0  10.7    5.8  -20.4  28.8   -6.9   12.8  -14.4   10.6   -2.4  
+ 9  140.4  46.4  C1exo  145.8  53.4  C2end  319.6  46.3  324.3   12.9   17.1  315.9   54.5  315.9   20.4   14.1  -17.9  13.0   -4.7   -5.7  14.8  -17.3   17.5  -11.5    2.5    8.7  
+10  145.8  53.4  C2end  147.5  47.0  C2end  315.9  54.5  315.9   20.4   14.1  322.3   47.0  320.3   18.6   11.7  -13.3  12.3   -7.0    0.0   7.8  -18.6   14.7   -6.8   -3.8   14.0  
+11  147.5  47.0  C2end  113.8  51.7  C1exo  322.3  47.0  320.3   18.6   11.7  307.1   44.2  339.1  350.6   39.1  -12.6  14.9  -12.6    5.6   4.3  -19.5   10.7    0.5  -11.7   19.6  
+12  153.2  43.1  C2end  128.5  45.0  C1exo  328.7  42.9  321.5   20.8    6.4  316.6   42.7  332.0    3.1   25.4   -8.8   9.7   -7.5    2.3   4.1  -16.6    5.7    4.6  -14.8   19.9  
+13  128.5  45.0  C1exo   67.7  43.8  C4exo  316.6  42.7  332.0    3.1   25.4  326.1    9.4   16.6  323.0   44.6  -17.8  10.0   -0.7   -9.9  17.6    5.7  -19.2   24.4  -21.8   10.4  
+14   67.7  43.8  C4exo  149.3  41.0  C2end  326.1   9.4   16.6  323.0   44.6  328.2   40.8  324.7   17.3    9.0  -14.2  11.6   -5.1   -3.1  11.0  -11.0    7.5   -2.5   -4.2    9.5  
+15  149.3  41.0  C2end  169.3  42.3  C2end  328.2  40.8  324.7   17.3    9.0  338.9   39.2  318.4   30.1  354.0   -9.3   7.6   -4.3   -1.3   6.6    5.8    2.2   -8.9   12.2  -11.5  
+16  169.3  42.3  C2end  147.0  42.3  C2end  338.9  39.2  318.4   30.1  354.0  326.3   43.1  324.5   16.5   10.6    1.4   6.0  -10.6   11.5  -8.3   -6.8    6.1   -2.8   -1.4    5.1  
+17  147.0  42.3  C2end  116.3  47.5  C1exo  326.3  43.1  324.5   16.5   10.6  312.0   42.0  339.0  353.4   34.5   -8.6  11.0   -8.4    3.5   3.1  -14.6    8.5    0.3   -8.9   15.0  
+18  116.3  47.5  C1exo  129.6  49.9  C1exo  312.0  42.0  339.0  353.4   34.5  312.2   48.2  328.2    4.7   27.1  -22.9   9.9    6.0  -19.6  27.0  -14.4   14.7  -10.5    2.5    7.6  
+19  129.6  49.9  C1exo  113.6  42.9  C1exo  312.2  48.2  328.2    4.7   27.1  316.4   36.8  342.8  352.0   32.7  -22.7  16.0   -4.7   -8.3  19.6  -10.1    3.3    4.2  -10.3   13.2  
+20  113.6  42.9  C1exo  156.3  51.0  C2end  316.4  36.8  342.8  352.0   32.7  324.4   50.9  313.3   27.9    4.3  -21.0   3.6   13.8  -26.6  30.3   -8.8   14.0  -14.0   10.0   -1.2  
+21  156.3  51.0  C2end  116.9  44.2  C1exo  324.4  50.9  313.3   27.9    4.3  315.3   39.3  340.0  354.3   31.7   -5.1   6.9   -6.2    3.4   0.8  -16.4   25.5  -24.9   15.8    0.5  
+22  116.9  44.2  C1exo   34.7  45.5  C3end  315.3  39.3  340.0  354.3   31.7  347.2  343.3   37.4  313.7   37.4  -19.0   6.6    7.3  -18.7  23.9   26.9  -45.3   45.2  -31.2    3.2  
+##
+#
+
diff --git a/package-lock.json b/package-lock.json
index 5acf9fba0766a64a8b2c37ec7bf200c239bf765c..1f882cb3578af5905d3e9d9c54e0c7fae325e90a 100644
Binary files a/package-lock.json and b/package-lock.json differ
diff --git a/package.json b/package.json
index ece9b6560bf7eecac8db21e68935222dcd031c0d..87946d0b46c5fd22acbd8e9c2a8690b840a22760 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "molstar",
-  "version": "3.11.0",
+  "version": "3.23.0",
   "description": "A comprehensive macromolecular library.",
   "homepage": "https://github.com/molstar/molstar#readme",
   "repository": {
@@ -20,7 +20,7 @@
     "rebuild": "npm run clean && npm run build",
     "build-viewer": "npm run build-tsc && npm run build-extra && npm run build-webpack-viewer",
     "build-tsc": "concurrently \"tsc --incremental\" \"tsc --build tsconfig.commonjs.json --incremental\"",
-    "build-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/",
+    "build-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/",
     "build-webpack": "webpack --mode production --config ./webpack.config.production.js",
     "build-webpack-viewer": "webpack --mode production --config ./webpack.config.viewer.js",
     "watch": "concurrently -c \"green,green,gray,gray\" --names \"tsc,srv,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-servers\" \"npm:watch-extra\" \"npm:watch-webpack\"",
@@ -28,7 +28,7 @@
     "watch-viewer-debug": "concurrently -c \"green,gray,gray\" --names \"tsc,ext,wpc\" --kill-others \"npm:watch-tsc\" \"npm:watch-extra\" \"npm:watch-webpack-viewer-debug\"",
     "watch-tsc": "tsc --watch --incremental",
     "watch-servers": "tsc --build tsconfig.commonjs.json --watch --incremental",
-    "watch-extra": "cpx \"src/**/*.{scss,html,ico}\" lib/ --watch",
+    "watch-extra": "cpx \"src/**/*.{scss,html,ico,jpg}\" lib/ --watch",
     "watch-webpack": "webpack -w --mode development --stats minimal",
     "watch-webpack-viewer": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.js",
     "watch-webpack-viewer-debug": "webpack -w --mode development --stats minimal --config ./webpack.config.viewer.debug.js",
@@ -75,7 +75,9 @@
       "node_modules",
       "lib"
     ],
-    "testURL": "http://localhost/",
+    "testEnvironmentOptions": {
+      "url": "http://localhost/"
+    },
     "testRegex": "\\.spec\\.ts$"
   },
   "author": "Mol* Contributors",
@@ -87,70 +89,73 @@
     "Ludovic Autin <autin@scripps.edu>",
     "Michal Malý <michal.maly@ibt.cas.cz>",
     "Jiří Černý <jiri.cerny@ibt.cas.cz>",
-    "Panagiotis Tourlas <panagiot_tourlov@hotmail.com>"
+    "Panagiotis Tourlas <panagiot_tourlov@hotmail.com>",
+    "Adam Midlik <midlik@gmail.com>",
+    "Koya Sakuma <koya.sakuma.work@gmail.com>",
+    "Gianluca Tomasello <giagitom@gmail.com>"
   ],
   "license": "MIT",
   "devDependencies": {
-    "@graphql-codegen/add": "^3.1.1",
-    "@graphql-codegen/cli": "^2.6.2",
-    "@graphql-codegen/time": "^3.1.1",
-    "@graphql-codegen/typescript": "^2.5.1",
-    "@graphql-codegen/typescript-graphql-files-modules": "^2.1.1",
-    "@graphql-codegen/typescript-graphql-request": "^4.4.10",
-    "@graphql-codegen/typescript-operations": "^2.4.2",
+    "@graphql-codegen/add": "^3.2.1",
+    "@graphql-codegen/cli": "^2.13.7",
+    "@graphql-codegen/time": "^3.2.1",
+    "@graphql-codegen/typescript": "^2.7.4",
+    "@graphql-codegen/typescript-graphql-files-modules": "^2.2.1",
+    "@graphql-codegen/typescript-graphql-request": "^4.5.6",
+    "@graphql-codegen/typescript-operations": "^2.5.4",
     "@types/cors": "^2.8.12",
-    "@types/gl": "^4.1.0",
-    "@types/jest": "^28.1.3",
-    "@types/react": "^18.0.14",
-    "@types/react-dom": "^18.0.5",
-    "@typescript-eslint/eslint-plugin": "^5.29.0",
-    "@typescript-eslint/parser": "^5.29.0",
+    "@types/gl": "^4.1.1",
+    "@types/jest": "^29.1.2",
+    "@types/react": "^18.0.21",
+    "@types/react-dom": "^18.0.6",
+    "@typescript-eslint/eslint-plugin": "^5.40.0",
+    "@typescript-eslint/parser": "^5.40.0",
     "benchmark": "^2.1.4",
-    "concurrently": "^7.2.2",
+    "concurrently": "^7.4.0",
     "cpx2": "^4.2.0",
     "crypto-browserify": "^3.12.0",
     "css-loader": "^6.7.1",
-    "eslint": "^8.18.0",
+    "eslint": "^8.25.0",
     "extra-watch-webpack-plugin": "^1.0.3",
     "file-loader": "^6.2.0",
     "fs-extra": "^10.1.0",
-    "graphql": "^16.5.0",
+    "graphql": "^16.6.0",
     "http-server": "^14.1.1",
-    "jest": "^28.1.1",
+    "jest": "^29.2.0",
     "mini-css-extract-plugin": "^2.6.1",
     "path-browserify": "^1.0.1",
     "raw-loader": "^4.0.2",
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
-    "sass": "^1.53.0",
-    "sass-loader": "^13.0.1",
-    "simple-git": "^3.10.0",
+    "sass": "^1.55.0",
+    "sass-loader": "^13.1.0",
+    "simple-git": "^3.14.1",
     "stream-browserify": "^3.0.0",
     "style-loader": "^3.3.1",
-    "ts-jest": "^28.0.5",
-    "typescript": "^4.7.4",
-    "webpack": "^5.73.0",
+    "ts-jest": "^29.0.3",
+    "typescript": "^4.8.4",
+    "webpack": "^5.74.0",
     "webpack-cli": "^4.10.0"
   },
   "dependencies": {
     "@types/argparse": "^2.0.10",
-    "@types/benchmark": "^2.1.1",
+    "@types/benchmark": "^2.1.2",
     "@types/compression": "1.7.2",
-    "@types/express": "^4.17.13",
-    "@types/node": "^16.11.41",
+    "@types/express": "^4.17.14",
+    "@types/node": "^16.11.66",
     "@types/node-fetch": "^2.6.2",
     "@types/swagger-ui-dist": "3.30.1",
     "argparse": "^2.0.1",
-    "body-parser": "^1.20.0",
+    "body-parser": "^1.20.1",
     "compression": "^1.7.4",
     "cors": "^2.8.5",
-    "express": "^4.18.1",
+    "express": "^4.18.2",
     "h264-mp4-encoder": "^1.0.12",
     "immer": "^9.0.15",
     "immutable": "^4.1.0",
     "node-fetch": "^2.6.7",
-    "rxjs": "^7.5.5",
-    "swagger-ui-dist": "^4.12.0",
+    "rxjs": "^7.5.7",
+    "swagger-ui-dist": "^4.14.3",
     "tslib": "^2.4.0",
     "util.promisify": "^1.1.1",
     "xhr2": "^0.2.1"
diff --git a/src/apps/docking-viewer/index.ts b/src/apps/docking-viewer/index.ts
index 8242e62030159354074c3e99d9e3f95bea9b57a9..2d7f5d71c97b7ed6a7aa473727b1143ef74935e5 100644
--- a/src/apps/docking-viewer/index.ts
+++ b/src/apps/docking-viewer/index.ts
@@ -58,20 +58,22 @@ class Viewer {
     }
 
     static async create(elementOrId: string | HTMLElement, colors = [Color(0x992211), Color(0xDDDDDD)], showButtons = true) {
-        const o = { ...DefaultViewerOptions, ...{
-            layoutIsExpanded: false,
-            layoutShowControls: false,
-            layoutShowRemoteState: false,
-            layoutShowSequence: true,
-            layoutShowLog: false,
-            layoutShowLeftPanel: true,
-
-            viewportShowExpand: true,
-            viewportShowControls: false,
-            viewportShowSettings: false,
-            viewportShowSelectionMode: false,
-            viewportShowAnimation: false,
-        } };
+        const o = {
+            ...DefaultViewerOptions, ...{
+                layoutIsExpanded: false,
+                layoutShowControls: false,
+                layoutShowRemoteState: false,
+                layoutShowSequence: true,
+                layoutShowLog: false,
+                layoutShowLeftPanel: true,
+
+                viewportShowExpand: true,
+                viewportShowControls: false,
+                viewportShowSettings: false,
+                viewportShowSelectionMode: false,
+                viewportShowAnimation: false,
+            }
+        };
         const defaultSpec = DefaultPluginUISpec();
 
         const spec: PluginUISpec = {
@@ -135,18 +137,16 @@ class Viewer {
             }
         };
 
-        plugin.behaviors.canvas3d.initialized.subscribe(v => {
-            if (v) {
-                PluginCommands.Canvas3D.SetSettings(plugin, { settings: {
-                    renderer: {
-                        ...plugin.canvas3d!.props.renderer,
-                        backgroundColor: ColorNames.white,
-                    },
-                    camera: {
-                        ...plugin.canvas3d!.props.camera,
-                        helper: { axes: { name: 'off', params: {} } }
-                    }
-                } });
+        PluginCommands.Canvas3D.SetSettings(plugin, {
+            settings: {
+                renderer: {
+                    ...plugin.canvas3d!.props.renderer,
+                    backgroundColor: ColorNames.white,
+                },
+                camera: {
+                    ...plugin.canvas3d!.props.camera,
+                    helper: { axes: { name: 'off', params: {} } }
+                }
             }
         });
 
@@ -166,7 +166,7 @@ class Viewer {
             structures.push({ ref: structureProperties?.ref || structure.ref });
         }
 
-        // remove current structuresfrom hierarchy as they will be merged
+        // remove current structures from hierarchy as they will be merged
         // TODO only works with using loadStructuresFromUrlsAndMerge once
         //      need some more API metho to work with the hierarchy
         this.plugin.managers.structure.hierarchy.updateCurrent(this.plugin.managers.structure.hierarchy.current.structures, 'remove');
diff --git a/src/apps/docking-viewer/viewport.tsx b/src/apps/docking-viewer/viewport.tsx
index 78e8412b5ef4dc70f035bf8a3bb03c2f6a379a4e..67976ac1ef065cd73fc79f58c8e1e57bb263d487 100644
--- a/src/apps/docking-viewer/viewport.tsx
+++ b/src/apps/docking-viewer/viewport.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -202,14 +202,14 @@ const InteractionsPreset = StructureRepresentationPresetProvider({
         const components = {
             ligand: await presetStaticComponent(plugin, structureCell, 'ligand'),
             surroundings: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandSurroundings, `surroundings`),
-            interactions: await plugin.builders.structure.tryCreateComponentFromSelection(structureCell, ligandPlusSurroundings, `interactions`)
+            interactions: await presetStaticComponent(plugin, structureCell, 'ligand'),
         };
 
         const { update, builder, typeParams } = StructureRepresentationPresetProvider.reprBuilder(plugin, params);
         const representations = {
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.3 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ligand' }),
             ballAndStick: builder.buildRepresentation(update, components.surroundings, { type: 'ball-and-stick', typeParams: { ...typeParams, material: CustomMaterial, sizeFactor: 0.1, sizeAspectRatio: 1 }, color: 'element-symbol', colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ball-and-stick' }),
-            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
+            interactions: builder.buildRepresentation(update, components.interactions, { type: InteractionsRepresentationProvider, typeParams: { ...typeParams, material: CustomMaterial, includeParent: true, parentDisplay: 'between' }, color: InteractionTypeColorThemeProvider }, { tag: 'interactions' }),
             label: builder.buildRepresentation(update, components.surroundings, { type: 'label', typeParams: { ...typeParams, material: CustomMaterial, background: false, borderWidth: 0.1 }, color: 'uniform', colorParams: { value: Color(0x000000) } }, { tag: 'label' }),
         };
 
diff --git a/src/apps/viewer/app.ts b/src/apps/viewer/app.ts
index 2ab5c5eafb53157ff9ca975cb4041cbf990f8167..93920c9d94511f926731d070b237c4ca0012c379 100644
--- a/src/apps/viewer/app.ts
+++ b/src/apps/viewer/app.ts
@@ -46,6 +46,7 @@ import { Color } from '../../mol-util/color';
 import '../../mol-util/polyfill';
 import { ObjectKeys } from '../../mol-util/type-helpers';
 import { SaccharideCompIdMapType } from '../../mol-model/structure/structure/carbohydrates/constants';
+import { Backgrounds } from '../../extensions/backgrounds';
 
 export { PLUGIN_VERSION as version } from '../../mol-plugin/version';
 export { setDebugMode, setProductionMode, setTimingMode } from '../../mol-util/debug';
@@ -55,6 +56,7 @@ const CustomFormats = [
 ];
 
 const Extensions = {
+    'backgrounds': PluginSpec.Behavior(Backgrounds),
     'cellpack': PluginSpec.Behavior(CellPack),
     'dnatco-confal-pyramids': PluginSpec.Behavior(DnatcoConfalPyramids),
     'pdbe-structure-quality-report': PluginSpec.Behavior(PDBeStructureQualityReport),
@@ -86,7 +88,9 @@ const DefaultViewerOptions = {
     pickScale: PluginConfig.General.PickScale.defaultValue,
     pickPadding: PluginConfig.General.PickPadding.defaultValue,
     enableWboit: PluginConfig.General.EnableWboit.defaultValue,
+    enableDpoit: PluginConfig.General.EnableDpoit.defaultValue,
     preferWebgl1: PluginConfig.General.PreferWebGl1.defaultValue,
+    allowMajorPerformanceCaveat: PluginConfig.General.AllowMajorPerformanceCaveat.defaultValue,
 
     viewportShowExpand: PluginConfig.Viewport.ShowExpand.defaultValue,
     viewportShowControls: PluginConfig.Viewport.ShowControls.defaultValue,
@@ -156,7 +160,9 @@ export class Viewer {
                 [PluginConfig.General.PickScale, o.pickScale],
                 [PluginConfig.General.PickPadding, o.pickPadding],
                 [PluginConfig.General.EnableWboit, o.enableWboit],
+                [PluginConfig.General.EnableDpoit, o.enableDpoit],
                 [PluginConfig.General.PreferWebGl1, o.preferWebgl1],
+                [PluginConfig.General.AllowMajorPerformanceCaveat, o.allowMajorPerformanceCaveat],
                 [PluginConfig.Viewport.ShowExpand, o.viewportShowExpand],
                 [PluginConfig.Viewport.ShowControls, o.viewportShowControls],
                 [PluginConfig.Viewport.ShowSettings, o.viewportShowSettings],
@@ -197,7 +203,7 @@ export class Viewer {
         return PluginCommands.State.Snapshots.OpenUrl(this.plugin, { url, type });
     }
 
-    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions) {
+    loadStructureFromUrl(url: string, format: BuiltInTrajectoryFormat = 'mmcif', isBinary = false, options?: LoadStructureOptions & { label?: string }) {
         const params = DownloadStructure.createDefaultParams(this.plugin.state.data.root.obj!, this.plugin);
         return this.plugin.runTask(this.plugin.state.data.applyAction(DownloadStructure, {
             source: {
@@ -206,6 +212,7 @@ export class Viewer {
                     url: Asset.Url(url),
                     format: format as any,
                     isBinary,
+                    label: options?.label,
                     options: { ...params.source.params.options, representationParams: options?.representationParams as any },
                 }
             }
@@ -494,4 +501,4 @@ export const ViewerAutoPreset = StructureRepresentationPresetProvider({
             return await PresetStructureRepresentations.auto.apply(ref, params, plugin);
         }
     }
-});
\ No newline at end of file
+});
diff --git a/src/apps/viewer/embedded.html b/src/apps/viewer/embedded.html
index 8533dc365cab388d7d2dae84f216ff55f0840c42..a309a84bdec9dec2d8e9d13488e5f38a8cd5fb0e 100644
--- a/src/apps/viewer/embedded.html
+++ b/src/apps/viewer/embedded.html
@@ -38,6 +38,15 @@
                 viewer.loadPdb('7bv2');
                 viewer.loadEmdb('EMD-30210', { detail: 6 });
                 // viewer.loadAllModelsOrAssemblyFromUrl('https://cs.litemol.org/5ire/full', 'mmcif', false, { representationParams: { theme: { globalName: 'operator-name' } } })
+                // viewer.loadStructureFromUrl('my url', 'pdb', false, {
+                //     representationParams: {
+                //         theme: {
+                //             globalName: 'uniform',
+                //             globalColorParams: { value: 0xff0000 }
+                //         }
+                //     },
+                //     label: 'my structure'
+                // });
             });
         </script>
     </body>
diff --git a/src/apps/viewer/index.html b/src/apps/viewer/index.html
index c63978410e9ee354bfa5ac2d916dd2104d941b1d..71d41ff6787f13b3ac4a0f40a3d57a1a0fd25cad 100644
--- a/src/apps/viewer/index.html
+++ b/src/apps/viewer/index.html
@@ -60,7 +60,9 @@
             var pickScale = getParam('pick-scale', '[^&]+').trim();
             var pickPadding = getParam('pick-padding', '[^&]+').trim();
             var disableWboit = getParam('disable-wboit', '[^&]+').trim() === '1';
+            var enableDpoit = getParam('enable-dpoit', '[^&]+').trim() === '1';
             var preferWebgl1 = getParam('prefer-webgl1', '[^&]+').trim() === '1' || void 0;
+            var allowMajorPerformanceCaveat = getParam('allow-major-performance-caveat', '[^&]+').trim() === '1';
 
             molstar.Viewer.create('app', {
                 layoutShowControls: !hideControls,
@@ -74,8 +76,10 @@
                 pixelScale: parseFloat(pixelScale) || 1,
                 pickScale: parseFloat(pickScale) || 0.25,
                 pickPadding: isNaN(parseFloat(pickPadding)) ? 1 : parseFloat(pickPadding),
-                enableWboit: disableWboit ? false : void 0, // use default value if disable-wboit is not set
+                enableWboit: (disableWboit || enableDpoit) ? false : void 0, // use default value if disable-wboit is not set
+                enableDpoit: enableDpoit ? true : void 0,
                 preferWebgl1: preferWebgl1,
+                allowMajorPerformanceCaveat: allowMajorPerformanceCaveat,
             }).then(viewer => {
                 var snapshotId = getParam('snapshot-id', '[^&]+').trim();
                 if (snapshotId) viewer.setRemoteSnapshot(snapshotId);
diff --git a/src/cli/chem-comp-dict/create-ions.ts b/src/cli/chem-comp-dict/create-ions.ts
index dcb798d47f71745a11b07cc3356cebc625280077..5ad5c784dee66da3bad9602473d18168d08aa4e6 100644
--- a/src/cli/chem-comp-dict/create-ions.ts
+++ b/src/cli/chem-comp-dict/create-ions.ts
@@ -15,7 +15,7 @@ const writeFile = util.promisify(fs.writeFile);
 
 import { DatabaseCollection } from '../../mol-data/db';
 import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
-import { ensureDataAvailable, readCCD } from './util';
+import { DefaultDataOptions, ensureDataAvailable, readCCD } from './util';
 
 function extractIonNames(ccd: DatabaseCollection<CCD_Schema>) {
     const ionNames: string[] = [];
@@ -44,8 +44,8 @@ export const IonNames = new Set(${JSON.stringify(ionNames).replace(/"/g, "'").re
     writeFile(filePath, output);
 }
 
-async function run(out: string, forceDownload = false) {
-    await ensureDataAvailable(forceDownload);
+async function run(out: string, options = DefaultDataOptions) {
+    await ensureDataAvailable(options);
     const ccd = await readCCD();
     const ionNames = extractIonNames(ccd);
     if (!fs.existsSync(path.dirname(out))) {
@@ -65,10 +65,15 @@ parser.add_argument('--forceDownload', '-f', {
     action: 'store_true',
     help: 'Force download of CCD and PVCD.'
 });
+parser.add_argument('--ccdUrl', '-c', {
+    help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
+    required: false
+});
 interface Args {
     out: string,
     forceDownload?: boolean,
+    ccdUrl?: string
 }
 const args: Args = parser.parse_args();
 
-run(args.out, args.forceDownload);
+run(args.out, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl });
diff --git a/src/cli/chem-comp-dict/create-saccharides.ts b/src/cli/chem-comp-dict/create-saccharides.ts
index 5850522d09e95f3f4643a916acadec82aa0a1ac9..dd629b65918a7ad7f0762647c650c0074b4ce526 100644
--- a/src/cli/chem-comp-dict/create-saccharides.ts
+++ b/src/cli/chem-comp-dict/create-saccharides.ts
@@ -14,7 +14,7 @@ const writeFile = util.promisify(fs.writeFile);
 
 import { DatabaseCollection } from '../../mol-data/db';
 import { CCD_Schema } from '../../mol-io/reader/cif/schema/ccd';
-import { ensureDataAvailable, readCCD } from './util';
+import { DefaultDataOptions, ensureDataAvailable, readCCD } from './util';
 
 function extractSaccharideNames(ccd: DatabaseCollection<CCD_Schema>) {
     const saccharideNames: string[] = [];
@@ -47,8 +47,8 @@ export const SaccharideNames = new Set(${JSON.stringify(ionNames).replace(/"/g,
     writeFile(filePath, output);
 }
 
-async function run(out: string, forceDownload = false) {
-    await ensureDataAvailable(forceDownload);
+async function run(out: string, options = DefaultDataOptions) {
+    await ensureDataAvailable(options);
     const ccd = await readCCD();
     const saccharideNames = extractSaccharideNames(ccd);
     if (!fs.existsSync(path.dirname(out))) {
@@ -68,10 +68,15 @@ parser.add_argument('--forceDownload', '-f', {
     action: 'store_true',
     help: 'Force download of CCD and PVCD.'
 });
+parser.add_argument('--ccdUrl', '-c', {
+    help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
+    required: false
+});
 interface Args {
     out: string,
     forceDownload?: boolean,
+    ccdUrl?: string
 }
 const args: Args = parser.parse_args();
 
-run(args.out, args.forceDownload);
+run(args.out, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl });
diff --git a/src/cli/chem-comp-dict/create-table.ts b/src/cli/chem-comp-dict/create-table.ts
index 7cf2aeade71d069980e6d809bdb903ed74fa48ac..f26b884e3d73009ff8b438de66f7255b04aa4a10 100644
--- a/src/cli/chem-comp-dict/create-table.ts
+++ b/src/cli/chem-comp-dict/create-table.ts
@@ -18,7 +18,7 @@ import { SetUtils } from '../../mol-util/set';
 import { DefaultMap } from '../../mol-util/map';
 import { mmCIF_chemCompBond_schema } from '../../mol-io/reader/cif/schema/mmcif-extras';
 import { ccd_chemCompAtom_schema } from '../../mol-io/reader/cif/schema/ccd-extras';
-import { ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
+import { DefaultDataOptions, ensureDataAvailable, getEncodedCif, readCCD, readPVCD } from './util';
 
 type CCB = Table<CCD_Schema['chem_comp_bond']>
 type CCA = Table<CCD_Schema['chem_comp_atom']>
@@ -239,8 +239,8 @@ function createAtoms(ccd: DatabaseCollection<CCD_Schema>, pvcd: DatabaseCollecti
     );
 }
 
-async function run(out: string, binary = false, forceDownload = false, ccaOut?: string) {
-    await ensureDataAvailable(forceDownload);
+async function run(out: string, binary = false, options = DefaultDataOptions, ccaOut?: string) {
+    await ensureDataAvailable(options);
     const ccd = await readCCD();
     const pvcd = await readPVCD();
 
@@ -283,12 +283,22 @@ parser.add_argument('--ccaOut', '-a', {
     help: 'Optional generated file output path for chem_comp_atom data.',
     required: false
 });
+parser.add_argument('--ccdUrl', '-c', {
+    help: 'Fetch the CCD from a custom URL. This forces download of the CCD.',
+    required: false
+});
+parser.add_argument('--pvcdUrl', '-p', {
+    help: 'Fetch the PVCD from a custom URL. This forces download of the PVCD.',
+    required: false
+});
 interface Args {
     out: string,
     forceDownload?: boolean,
     binary?: boolean,
-    ccaOut?: string
+    ccaOut?: string,
+    ccdUrl?: string,
+    pvcdUrl?: string
 }
 const args: Args = parser.parse_args();
 
-run(args.out, args.binary, args.forceDownload, args.ccaOut);
+run(args.out, args.binary, { forceDownload: args.forceDownload, ccdUrl: args.ccdUrl, pvcdUrl: args.pvcdUrl }, args.ccaOut);
diff --git a/src/cli/chem-comp-dict/util.ts b/src/cli/chem-comp-dict/util.ts
index 5de81ee16b1e67812b03af06083b20195e7dc9c9..751f2005744cc0637d4d8d21e0a954d30c189a8b 100644
--- a/src/cli/chem-comp-dict/util.ts
+++ b/src/cli/chem-comp-dict/util.ts
@@ -35,9 +35,9 @@ export async function ensureAvailable(path: string, url: string, forceDownload =
     }
 }
 
-export async function ensureDataAvailable(forceDownload = false) {
-    await ensureAvailable(CCD_PATH, CCD_URL, forceDownload);
-    await ensureAvailable(PVCD_PATH, PVCD_URL, forceDownload);
+export async function ensureDataAvailable(options: DataOptions) {
+    await ensureAvailable(CCD_PATH, options.ccdUrl || CCD_URL, !!options.ccdUrl || options.forceDownload);
+    await ensureAvailable(PVCD_PATH, options.pvcdUrl || PVCD_URL, !!options.pvcdUrl || options.forceDownload);
 }
 
 export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) {
@@ -68,6 +68,16 @@ export function getEncodedCif(name: string, database: Database<Database.Schema>,
     return encoder.getData();
 }
 
+export type DataOptions = {
+    ccdUrl?: string,
+    pvcdUrl?: string,
+    forceDownload?: boolean
+}
+
+export const DefaultDataOptions: DataOptions = {
+    forceDownload: false
+};
+
 const DATA_DIR = path.join(__dirname, '..', '..', '..', '..', 'build/data');
 const CCD_PATH = path.join(DATA_DIR, 'components.cif');
 const PVCD_PATH = path.join(DATA_DIR, 'aa-variants-v1.cif');
diff --git a/src/cli/cifschema/util/cif-dic.ts b/src/cli/cifschema/util/cif-dic.ts
index 6bf344e2f301f1c73c78e0002d8df39729a02f64..2f5f75efc7d4f66a04d1f81711699587c0bcd914 100644
--- a/src/cli/cifschema/util/cif-dic.ts
+++ b/src/cli/cifschema/util/cif-dic.ts
@@ -71,6 +71,7 @@ export function getFieldType(type: string, description: string, values?: string[
         case 'ec-type':
         case 'ucode-alphanum-csv':
         case 'id_list':
+        case 'entity_id_list':
             return ListCol('str', ',', description);
         case 'id_list_spc':
             return ListCol('str', ' ', description);
diff --git a/src/examples/alpha-orbitals/controls.tsx b/src/examples/alpha-orbitals/controls.tsx
index bbde98561a7f76137e9b7456338871f3a1efe9dc..11c8147d9453dcd1107a33a760c3c7ed600bc5c7 100644
--- a/src/examples/alpha-orbitals/controls.tsx
+++ b/src/examples/alpha-orbitals/controls.tsx
@@ -4,16 +4,16 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import * as ReactDOM from 'react-dom';
+import { createRoot } from 'react-dom/client';
 import { AlphaOrbitalsExample } from '.';
 import { ParameterControls } from '../../mol-plugin-ui/controls/parameters';
 import { useBehavior } from '../../mol-plugin-ui/hooks/use-behavior';
 import { PluginContextContainer } from '../../mol-plugin-ui/plugin';
 
 export function mountControls(orbitals: AlphaOrbitalsExample, parent: Element) {
-    ReactDOM.render(<PluginContextContainer plugin={orbitals.plugin}>
+    createRoot(parent).render(<PluginContextContainer plugin={orbitals.plugin}>
         <Controls orbitals={orbitals} />
-    </PluginContextContainer>, parent);
+    </PluginContextContainer>);
 }
 
 function Controls({ orbitals }: { orbitals: AlphaOrbitalsExample }) {
diff --git a/src/examples/alpha-orbitals/index.ts b/src/examples/alpha-orbitals/index.ts
index 7239bcc511664789ab4878dab94cf4b8dc8b2564..cf463bfeefd8d92aa74137c562be5d416d286983 100644
--- a/src/examples/alpha-orbitals/index.ts
+++ b/src/examples/alpha-orbitals/index.ts
@@ -82,24 +82,20 @@ export class AlphaOrbitalsExample {
 
         this.plugin.managers.interactivity.setProps({ granularity: 'element' });
 
-        this.plugin.behaviors.canvas3d.initialized.subscribe(init => {
-            if (!init) return;
-
-            if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) {
-                PluginCommands.Toast.Show(this.plugin, {
-                    title: 'Error',
-                    message: `Browser/device does not support required WebGL extension (OES_texture_float).`
-                });
-                return;
-            }
-
-            this.load({
-                moleculeSdf: DemoMoleculeSDF,
-                ...DemoOrbitals
+        if (!canComputeGrid3dOnGPU(this.plugin.canvas3d?.webgl)) {
+            PluginCommands.Toast.Show(this.plugin, {
+                title: 'Error',
+                message: `Browser/device does not support required WebGL extension (OES_texture_float).`
             });
+            return;
+        }
 
-            mountControls(this, document.getElementById('controls')!);
+        this.load({
+            moleculeSdf: DemoMoleculeSDF,
+            ...DemoOrbitals
         });
+
+        mountControls(this, document.getElementById('controls')!);
     }
 
     readonly params = new BehaviorSubject<ParamDefinition.For<Params>>({} as any);
diff --git a/src/extensions/backgrounds/images/cells.jpg b/src/extensions/backgrounds/images/cells.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3502c798c5d012acd67640ca8cbbb2341014c71f
Binary files /dev/null and b/src/extensions/backgrounds/images/cells.jpg differ
diff --git a/src/extensions/backgrounds/index.ts b/src/extensions/backgrounds/index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c5bbdc99e1f013f3658ffa660cc0df4889db5e44
--- /dev/null
+++ b/src/extensions/backgrounds/index.ts
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { PluginBehavior } from '../../mol-plugin/behavior/behavior';
+import { PluginConfig } from '../../mol-plugin/config';
+import { Color } from '../../mol-util/color/color';
+
+// from https://visualsonline.cancer.gov/details.cfm?imageid=2304, public domain
+import image_cells from './images/cells.jpg';
+
+// created with http://alexcpeterson.com/spacescape/
+import face_nebula_nx from './skyboxes/nebula/nebula_left2.jpg';
+import face_nebula_ny from './skyboxes/nebula/nebula_bottom4.jpg';
+import face_nebula_nz from './skyboxes/nebula/nebula_back6.jpg';
+import face_nebula_px from './skyboxes/nebula/nebula_right1.jpg';
+import face_nebula_py from './skyboxes/nebula/nebula_top3.jpg';
+import face_nebula_pz from './skyboxes/nebula/nebula_front5.jpg';
+
+export const Backgrounds = PluginBehavior.create<{ }>({
+    name: 'extension-backgrounds',
+    category: 'misc',
+    display: {
+        name: 'Backgrounds'
+    },
+    ctor: class extends PluginBehavior.Handler<{ }> {
+        register(): void {
+            this.ctx.config.set(PluginConfig.Background.Styles, [
+                [{
+                    variant: {
+                        name: 'radialGradient',
+                        params: {
+                            centerColor: Color(0xFFFFFF),
+                            edgeColor: Color(0x808080),
+                            ratio: 0.2,
+                            coverage: 'viewport',
+                        }
+                    }
+                }, 'Light Radial Gradient'],
+                [{
+                    variant: {
+                        name: 'image',
+                        params: {
+                            source: {
+                                name: 'url',
+                                params: image_cells
+                            },
+                            lightness: 0,
+                            saturation: 0,
+                            opacity: 1,
+                            coverage: 'viewport',
+                        }
+                    }
+                }, 'Normal Cells Image'],
+                [{
+                    variant: {
+                        name: 'skybox',
+                        params: {
+                            faces: {
+                                name: 'urls',
+                                params: {
+                                    nx: face_nebula_nx,
+                                    ny: face_nebula_ny,
+                                    nz: face_nebula_nz,
+                                    px: face_nebula_px,
+                                    py: face_nebula_py,
+                                    pz: face_nebula_pz,
+                                }
+                            },
+                            lightness: 0,
+                            saturation: 0,
+                            opacity: 1,
+                            blur: 0.3,
+                        }
+                    }
+                }, 'Purple Nebula Skybox'],
+            ]);
+        }
+
+        update() {
+            return false;
+        }
+
+        unregister() {
+            this.ctx.config.set(PluginConfig.Background.Styles, []);
+        }
+    },
+    params: () => ({ })
+});
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..4e2f0fd8d977272a4bc2af9cf982fe866eb52186
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_back6.jpg differ
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..2be6e805e2ea0103230cf51637218e115cf0a76d
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_bottom4.jpg differ
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..e9c0674db6f28f3d0fa02421e473517790d51f2d
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_front5.jpg differ
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..810037b66dc4601b24b1b1b9adf514f626247b6c
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_left2.jpg differ
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..059d46bf8e7d8d18ce12259679f0c53ad4a5dc27
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_right1.jpg differ
diff --git a/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..831b81964a253fbbd08b0ba3aad74100ce55fad1
Binary files /dev/null and b/src/extensions/backgrounds/skyboxes/nebula/nebula_top3.jpg differ
diff --git a/src/extensions/backgrounds/typings.d.ts b/src/extensions/backgrounds/typings.d.ts
new file mode 100644
index 0000000000000000000000000000000000000000..83e4393576a0b35df23ec0583a74e20387a0b97e
--- /dev/null
+++ b/src/extensions/backgrounds/typings.d.ts
@@ -0,0 +1,10 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+declare module '*.jpg' {
+    const value: string;
+    export = value;
+}
diff --git a/src/extensions/cellpack/model.ts b/src/extensions/cellpack/model.ts
index a9babdc05b52181522f83db0b3569ed3a19a3b6a..80fb0b3d979a76bff36e18de6724ace45748852c 100644
--- a/src/extensions/cellpack/model.ts
+++ b/src/extensions/cellpack/model.ts
@@ -581,9 +581,20 @@ export const LoadCellPackModel = StateAction.build({
 })(({ state, params }, ctx: PluginContext) => Task.create('CellPack Loader', async taskCtx => {
     if (params.preset.adjustStyle) {
         ctx.managers.interactivity.setProps({ granularity: 'chain' });
+        ctx.managers.structure.component.setOptions({
+            ... ctx.managers.structure.component.state.options,
+            visualQuality: 'custom',
+            ignoreLight: true,
+            showHydrogens: false,
+        });
         ctx.canvas3d?.setProps({
             multiSample: { mode: 'off' },
             cameraClipping: { far: false },
+            renderer: { colorMarker: false },
+            marking: {
+                enabled: true,
+                ghostEdgeStrength: 1,
+            },
             postprocessing: {
                 occlusion: {
                     name: 'on',
diff --git a/src/extensions/dnatco/confal-pyramids/behavior.ts b/src/extensions/dnatco/confal-pyramids/behavior.ts
index 6293e2b6cd60b8710dd2c0c3f68ea074980b2b52..7c34704ae344e745aa3ad5b0e0fc948d199dd588 100644
--- a/src/extensions/dnatco/confal-pyramids/behavior.ts
+++ b/src/extensions/dnatco/confal-pyramids/behavior.ts
@@ -8,7 +8,7 @@
 import { ConfalPyramidsColorThemeProvider } from './color';
 import { ConfalPyramids, ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsRepresentationProvider } from './representation';
-import { Loci } from '../../../mol-model/loci';
+import { ConfalPyramidsTypes } from './types';
 import { PluginBehavior } from '../../../mol-plugin/behavior/behavior';
 import { StructureRepresentationPresetProvider, PresetStructureRepresentations } from '../../../mol-plugin-state/builder/structure/representation-preset';
 import { StateObjectRef } from '../../../mol-state';
@@ -56,21 +56,10 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
         description: 'Schematic depiction of conformer class and confal value.',
     },
     ctor: class extends PluginBehavior.Handler<{ autoAttach: boolean, showToolTip: boolean }> {
-
         private provider = ConfalPyramidsProvider;
 
-        private labelConfalPyramids = {
-            label: (loci: Loci): string | undefined => {
-                if (!this.params.showToolTip) return void 0;
-
-                /* TODO: Implement this */
-                return void 0;
-            }
-        };
-
         register(): void {
             this.ctx.customModelProperties.register(this.provider, this.params.autoAttach);
-            this.ctx.managers.lociLabels.addProvider(this.labelConfalPyramids);
 
             this.ctx.representation.structure.themes.colorThemeRegistry.add(ConfalPyramidsColorThemeProvider);
             this.ctx.representation.structure.registry.add(ConfalPyramidsRepresentationProvider);
@@ -88,7 +77,6 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
 
         unregister() {
             this.ctx.customModelProperties.unregister(ConfalPyramidsProvider.descriptor.name);
-            this.ctx.managers.lociLabels.removeProvider(this.labelConfalPyramids);
 
             this.ctx.representation.structure.registry.remove(ConfalPyramidsRepresentationProvider);
             this.ctx.representation.structure.themes.colorThemeRegistry.remove(ConfalPyramidsColorThemeProvider);
@@ -101,3 +89,13 @@ export const DnatcoConfalPyramids = PluginBehavior.create<{ autoAttach: boolean,
         showToolTip: PD.Boolean(true)
     })
 });
+
+export function confalPyramidLabel(halfPyramid: ConfalPyramidsTypes.HalfPyramid) {
+    const { step } = halfPyramid;
+    return `
+        <b>${step.auth_asym_id_1}</b> |
+        <b>${step.label_comp_id_1} ${step.auth_seq_id_1}${step.PDB_ins_code_1}${step.label_alt_id_1.length > 0 ? ` (alt ${step.label_alt_id_1})` : ''}
+           ${step.label_comp_id_2} ${step.auth_seq_id_2}${step.PDB_ins_code_2}${step.label_alt_id_2.length > 0 ? ` (alt ${step.label_alt_id_2})` : ''} </b><br />
+        <i>NtC:</i> ${step.NtC} | <i>Confal score:</i> ${step.confal_score} | <i>RMSD:</i> ${step.rmsd.toFixed(2)}
+    `;
+}
diff --git a/src/extensions/dnatco/confal-pyramids/color.ts b/src/extensions/dnatco/confal-pyramids/color.ts
index 8bc71f0f37a411013cb26c629561cf44aa1192dc..1cf4ad87052d33ce98c58c19fe73c39bc8f08726 100644
--- a/src/extensions/dnatco/confal-pyramids/color.ts
+++ b/src/extensions/dnatco/confal-pyramids/color.ts
@@ -247,8 +247,8 @@ export function ConfalPyramidsColorTheme(ctx: ThemeDataContext, props: PD.Values
 
     function color(location: Location, isSecondary: boolean): Color {
         if (CPT.isLocation(location)) {
-            const { pyramid, isLower } = location.data;
-            const key = pyramid.NtC + `_${isLower ? 'Lwr' : 'Upr'}` as keyof PyramidsColors;
+            const { step, isLower } = location.data;
+            const key = step.NtC + `_${isLower ? 'Lwr' : 'Upr'}` as keyof PyramidsColors;
             return colorMap[key] ?? ErrorColor;
         }
 
diff --git a/src/extensions/dnatco/confal-pyramids/property.ts b/src/extensions/dnatco/confal-pyramids/property.ts
index 4417ebbc18816abdf6cd7fece1af9077b7d88574..5ca7bada8ce41ae7c0ceb7c51291a891ce06ab8d 100644
--- a/src/extensions/dnatco/confal-pyramids/property.ts
+++ b/src/extensions/dnatco/confal-pyramids/property.ts
@@ -16,7 +16,7 @@ import { PropertyWrapper } from '../../../mol-model-props/common/wrapper';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { MmcifFormat } from '../../../mol-model-formats/structure/mmcif';
 
-export type ConfalPyramids = PropertyWrapper<CPT.PyramidsData | undefined >;
+export type ConfalPyramids = PropertyWrapper<CPT.Steps | undefined>;
 
 export namespace ConfalPyramids {
     export const Schema = {
@@ -105,34 +105,42 @@ export const ConfalPyramidsProvider: CustomModelProperty.Provider<ConfalPyramids
 
 type StepsSummaryTable = Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step_summary>;
 
-function createPyramidsFromCif(model: Model,
-    steps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
-    stepsSummary: StepsSummaryTable): CPT.PyramidsData {
-    const pyramids = new Array<CPT.Pyramid>();
-    const names = new Map<string, number>();
-    const locations = new Array<CPT.Location>();
-    let hasMultipleModels = false;
+function createPyramidsFromCif(
+    model: Model,
+    cifSteps: Table<typeof ConfalPyramids.Schema.ndb_struct_ntc_step>,
+    stepsSummary: StepsSummaryTable
+): CPT.Steps {
+    const steps = new Array<CPT.Step>();
+    const mapping = new Array<CPT.MappedChains>();
 
     const {
         id, PDB_model_number, name,
         auth_asym_id_1, auth_seq_id_1, label_comp_id_1, label_alt_id_1, PDB_ins_code_1,
         auth_asym_id_2, auth_seq_id_2, label_comp_id_2, label_alt_id_2, PDB_ins_code_2,
-        _rowCount } = steps;
+        _rowCount
+    } = cifSteps;
 
     if (_rowCount !== stepsSummary._rowCount) throw new Error('Inconsistent mmCIF data');
 
     for (let i = 0; i < _rowCount; i++) {
-        const model_num = PDB_model_number.value(i);
-        if (model_num !== model.modelNum)
-            hasMultipleModels = true;
-
-        const { _NtC, _confal_score } = getNtCAndConfalScore(id.value(i), i, stepsSummary);
-
-        const pyramid = {
-            PDB_model_number: model_num,
+        const {
+            NtC,
+            confal_score,
+            rmsd
+        } = getSummaryData(id.value(i), i, stepsSummary);
+        const modelNum = PDB_model_number.value(i);
+        const chainId = auth_asym_id_1.value(i);
+        const seqId = auth_seq_id_1.value(i);
+        const modelIdx = modelNum - 1;
+
+        if (mapping.length <= modelIdx || !mapping[modelIdx])
+            mapping[modelIdx] = new Map<string, CPT.MappedResidues>();
+
+        const step = {
+            PDB_model_number: modelNum,
             name: name.value(i),
-            auth_asym_id_1: auth_asym_id_1.value(i),
-            auth_seq_id_1: auth_seq_id_1.value(i),
+            auth_asym_id_1: chainId,
+            auth_seq_id_1: seqId,
             label_comp_id_1: label_comp_id_1.value(i),
             label_alt_id_1: label_alt_id_1.value(i),
             PDB_ins_code_1: PDB_ins_code_1.value(i),
@@ -141,30 +149,41 @@ function createPyramidsFromCif(model: Model,
             label_comp_id_2: label_comp_id_2.value(i),
             label_alt_id_2: label_alt_id_2.value(i),
             PDB_ins_code_2: PDB_ins_code_2.value(i),
-            confal_score: _confal_score,
-            NtC: _NtC
+            confal_score,
+            NtC,
+            rmsd,
         };
 
-        pyramids.push(pyramid);
-        names.set(pyramid.name, pyramids.length - 1);
+        steps.push(step);
 
-        locations.push(CPT.Location(pyramid, false));
-        locations.push(CPT.Location(pyramid, true));
+        const mappedChains = mapping[modelIdx];
+        const residuesOnChain = mappedChains.get(chainId) ?? new Map<number, number[]>();
+        const stepsForResidue = residuesOnChain.get(seqId) ?? [];
+        stepsForResidue.push(steps.length - 1);
+
+        residuesOnChain.set(seqId, stepsForResidue);
+        mappedChains.set(chainId, residuesOnChain);
+        mapping[modelIdx] = mappedChains;
     }
 
-    return { pyramids, names, locations, hasMultipleModels };
+    return { steps, mapping };
 }
 
-function getNtCAndConfalScore(id: number, i: number, stepsSummary: StepsSummaryTable) {
-    const { step_id, confal_score, assigned_NtC } = stepsSummary;
+function getSummaryData(id: number, i: number, stepsSummary: StepsSummaryTable) {
+    const {
+        step_id,
+        confal_score,
+        assigned_NtC,
+        cartesian_rmsd_closest_NtC_representative,
+    } = stepsSummary;
 
     // Assume that step_ids in ntc_step_summary are in the same order as steps in ntc_step
     for (let j = i; j < stepsSummary._rowCount; j++) {
-        if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) };
+        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
     }
     // Safety net for cases where the previous assumption is not met
     for (let j = 0; j < i; j++) {
-        if (id === step_id.value(j)) return { _NtC: assigned_NtC.value(j), _confal_score: confal_score.value(j) };
+        if (id === step_id.value(j)) return { NtC: assigned_NtC.value(j), confal_score: confal_score.value(j), rmsd: cartesian_rmsd_closest_NtC_representative.value(j) };
     }
     throw new Error('Inconsistent mmCIF data');
 }
diff --git a/src/extensions/dnatco/confal-pyramids/representation.ts b/src/extensions/dnatco/confal-pyramids/representation.ts
index b0ce90b53e355ad33fcf2379bbe4445949868ed8..1c7d4f95b1e05aaa06298becd8a4d7f89ecba911 100644
--- a/src/extensions/dnatco/confal-pyramids/representation.ts
+++ b/src/extensions/dnatco/confal-pyramids/representation.ts
@@ -6,7 +6,7 @@
  */
 
 import { ConfalPyramids, ConfalPyramidsProvider } from './property';
-import { ConfalPyramidsUtil } from './util';
+import { ConfalPyramidsIterator } from './util';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { Interval } from '../../../mol-data/int';
 import { Mesh } from '../../../mol-geo/geometry/mesh/mesh';
@@ -16,14 +16,14 @@ import { PrimitiveBuilder } from '../../../mol-geo/primitive/primitive';
 import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { Mat4, Vec3 } from '../../../mol-math/linear-algebra';
 import { EmptyLoci, Loci } from '../../../mol-model/loci';
-import { Structure, StructureProperties, Unit } from '../../../mol-model/structure';
+import { Structure, Unit } from '../../../mol-model/structure';
 import { CustomProperty } from '../../../mol-model-props/common/custom-property';
 import { Representation, RepresentationContext, RepresentationParamsGetter } from '../../../mol-repr/representation';
 import { StructureRepresentation, StructureRepresentationProvider, StructureRepresentationStateBuilder, UnitsRepresentation } from '../../../mol-repr/structure/representation';
 import { UnitsMeshParams, UnitsMeshVisual, UnitsVisual } from '../../../mol-repr/structure/units-visual';
 import { VisualUpdateState } from '../../../mol-repr/util';
 import { VisualContext } from '../../../mol-repr/visual';
-import { getAltResidueLociFromId, StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
 import { ParamDefinition as PD } from '../../../mol-util/param-definition';
 import { Theme, ThemeRegistryContext } from '../../../mol-theme/theme';
 import { NullLocation } from '../../../mol-model/location';
@@ -32,6 +32,12 @@ const t = Mat4.identity();
 const w = Vec3.zero();
 const mp = Vec3.zero();
 
+const posO3 = Vec3();
+const posP = Vec3();
+const posOP1 = Vec3();
+const posOP2 = Vec3();
+const posO5 = Vec3();
+
 function calcMidpoint(mp: Vec3, v: Vec3, w: Vec3) {
     Vec3.sub(mp, v, w);
     Vec3.scale(mp, mp, 0.5);
@@ -53,64 +59,76 @@ function createConfalPyramidsIterator(structureGroup: StructureGroup): LocationI
     const { structure, group } = structureGroup;
     const instanceCount = group.units.length;
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) {
-        return LocationIterator(0, 1, 1, () => NullLocation);
-    }
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return LocationIterator(0, 1, 1, () => NullLocation);
 
-    const { locations } = prop.data;
+    const halfPyramidsCount = data.steps.length * 2;
 
     const getLocation = (groupIndex: number, instanceIndex: number) => {
-        if (locations.length <= groupIndex) return NullLocation;
-        return locations[groupIndex];
+        if (halfPyramidsCount <= groupIndex) return NullLocation;
+        const idx = Math.floor(groupIndex / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
+        return CPT.Location(data.steps[idx], groupIndex % 2 === 1);
     };
-    return LocationIterator(locations.length, instanceCount, 1, getLocation);
+    return LocationIterator(halfPyramidsCount, instanceCount, 1, getLocation);
 }
 
 function createConfalPyramidsMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<ConfalPyramidsMeshParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) return Mesh.createEmpty(mesh);
-
-    const { pyramids } = prop.data;
-    if (pyramids.length === 0) return Mesh.createEmpty(mesh);
-
-    const mb = MeshBuilder.createState(512, 512, mesh);
-
-    const handler = (pyramid: CPT.Pyramid, first: ConfalPyramidsUtil.FirstResidueAtoms, second: ConfalPyramidsUtil.SecondResidueAtoms, firsLocIndex: number, secondLocIndex: number) => {
-        if (firsLocIndex === -1 || secondLocIndex === -1)
-            throw new Error('Invalid location index');
-
-        const scale = (pyramid.confal_score - 20.0) / 100.0;
-        const O3 = first.O3.pos;
-        const OP1 = second.OP1.pos; const OP2 = second.OP2.pos; const O5 = second.O5.pos; const P = second.P.pos;
-
-        shiftVertex(O3, P, scale);
-        shiftVertex(OP1, P, scale);
-        shiftVertex(OP2, P, scale);
-        shiftVertex(O5, P, scale);
-        calcMidpoint(mp, O3, O5);
-
-        mb.currentGroup = firsLocIndex;
-        let pb = PrimitiveBuilder(3);
-        /* Upper part (for first residue in step) */
-        pb.add(O3, OP1, OP2);
-        pb.add(O3, mp, OP1);
-        pb.add(O3, OP2, mp);
-        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
-
-        /* Lower part (for second residue in step */
-        mb.currentGroup = secondLocIndex;
-        pb = PrimitiveBuilder(3);
-        pb.add(mp, O5, OP1);
-        pb.add(mp, OP2, O5);
-        pb.add(O5, OP2, OP1);
-        MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
-    };
-
-    const walker = new ConfalPyramidsUtil.UnitWalker(structure, unit, handler);
-    walker.walk();
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return Mesh.createEmpty(mesh);
+
+    const { steps, mapping } = data;
+    if (steps.length === 0) return Mesh.createEmpty(mesh);
+    const vertexCount = (6 * steps.length) / mapping.length;
+
+    const mb = MeshBuilder.createState(vertexCount, vertexCount / 10, mesh);
+
+    const it = new ConfalPyramidsIterator(structure, unit);
+    while (it.hasNext) {
+        const allPoints = it.move();
+
+        for (const points of allPoints) {
+            const { O3, P, OP1, OP2, O5, confalScore } = points;
+            const scale = (confalScore - 20.0) / 100.0;
+            // Steps can be drawn in a different order than they are stored.
+            // To make sure that we can get from the drawn pyramid back to the step in represents,
+            // we need to use an appropriate groupId. The stepIdx passed from the iterator
+            // is an index into the array of all steps in the structure.
+            // Since a step is drawn as two "half-pyramids" we need two ids to map to a single step.
+            // To do that, we just multiply the index by 2. idx*2 marks the "upper" half-pyramid,
+            // (idx*2)+1 the "lower" half-pyramid.
+            const groupIdx = points.stepIdx * 2;
+
+            unit.conformation.invariantPosition(O3, posO3);
+            unit.conformation.invariantPosition(P, posP);
+            unit.conformation.invariantPosition(OP1, posOP1);
+            unit.conformation.invariantPosition(OP2, posOP2);
+            unit.conformation.invariantPosition(O5, posO5);
+
+            shiftVertex(posO3, posP, scale);
+            shiftVertex(posOP1, posP, scale);
+            shiftVertex(posOP2, posP, scale);
+            shiftVertex(posO5, posP, scale);
+            calcMidpoint(mp, posO3, posO5);
+
+            mb.currentGroup = groupIdx;
+            let pb = PrimitiveBuilder(3);
+            /* Upper part (for first residue in step) */
+            pb.add(posO3, posOP1, posOP2);
+            pb.add(posO3, mp, posOP1);
+            pb.add(posO3, posOP2, mp);
+            MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+
+            /* Lower part (for second residue in step) */
+            mb.currentGroup = groupIdx + 1;
+            pb = PrimitiveBuilder(3);
+            pb.add(mp, posO5, posOP1);
+            pb.add(mp, posOP2, posO5);
+            pb.add(posO5, posOP2, posOP1);
+            MeshBuilder.addPrimitive(mb, t, pb.getPrimitive());
+        }
+    }
 
     return MeshBuilder.getMesh(mb);
 }
@@ -124,16 +142,17 @@ function getConfalPyramidLoci(pickingId: PickingId, structureGroup: StructureGro
     const unit = structureGroup.group.units[instanceId];
     if (!Unit.isAtomic(unit)) return EmptyLoci;
 
-    const prop = ConfalPyramidsProvider.get(structure.model).value;
-    if (prop === undefined || prop.data === undefined) return EmptyLoci;
+    const data = ConfalPyramidsProvider.get(structure.model)?.value?.data;
+    if (!data) return EmptyLoci;
+
+    const halfPyramidsCount = data.steps.length * 2;
 
-    const { locations } = prop.data;
+    if (halfPyramidsCount <= groupId) return EmptyLoci;
 
-    if (locations.length <= groupId) return EmptyLoci;
-    const altId = StructureProperties.atom.label_alt_id(CPT.toElementLocation(locations[groupId]));
-    const rI = unit.residueIndex[locations[groupId].element.element];
+    const idx = Math.floor(groupId / 2); // Map groupIndex to a step, see createConfalPyramidsMesh() for full explanation
+    const step = data.steps[idx];
 
-    return getAltResidueLociFromId(structure, unit, rI, altId);
+    return CPT.Loci({ step, isLower: groupId % 2 === 1 }, [{}]);
 }
 
 function eachConfalPyramid(loci: Loci, structureGroup: StructureGroup, apply: (interval: Interval) => boolean) {
diff --git a/src/extensions/dnatco/confal-pyramids/types.ts b/src/extensions/dnatco/confal-pyramids/types.ts
index b7a94099885cb2aa5e37094cb6bb100c355b2392..f2b898822333d901e9d99789e1a93fd1ef4f4cf2 100644
--- a/src/extensions/dnatco/confal-pyramids/types.ts
+++ b/src/extensions/dnatco/confal-pyramids/types.ts
@@ -6,10 +6,13 @@
  */
 
 import { DataLocation } from '../../../mol-model/location';
-import { ElementIndex, Structure, StructureElement, Unit } from '../../../mol-model/structure';
+import { DataLoci } from '../../../mol-model/loci';
+import { confalPyramidLabel } from './behavior';
 
 export namespace ConfalPyramidsTypes {
-    export type Pyramid = {
+    export const DataTag = 'dnatco-confal-half-pyramid';
+
+    export type Step = {
         PDB_model_number: number,
         name: string,
         auth_asym_id_1: string,
@@ -23,38 +26,40 @@ export namespace ConfalPyramidsTypes {
         label_alt_id_2: string,
         PDB_ins_code_2: string,
         confal_score: number,
-        NtC: string
+        NtC: string,
+        rmsd: number,
     }
 
-    export interface PyramidsData {
-        pyramids: Array<Pyramid>,
-        names: Map<string, number>,
-        locations: Array<Location>,
-        hasMultipleModels: boolean
-    }
+    export type MappedChains = Map<string, MappedResidues>;
+    export type MappedResidues = Map<number, number[]>;
 
-    export interface LocationData {
-        readonly pyramid: Pyramid
-        readonly isLower: boolean;
+    export interface Steps {
+        steps: Array<Step>,
+        mapping: MappedChains[],
     }
 
-    export interface Element {
-        structure: Structure;
-        unit: Unit.Atomic;
-        element: ElementIndex;
+    export interface HalfPyramid {
+        step: Step,
+        isLower: boolean,
     }
 
-    export interface Location extends DataLocation<LocationData, Element> {}
+    export interface Location extends DataLocation<HalfPyramid, {}> {}
 
-    export function Location(pyramid: Pyramid, isLower: boolean, structure?: Structure, unit?: Unit.Atomic, element?: ElementIndex) {
-        return DataLocation('pyramid', { pyramid, isLower }, { structure: structure as any, unit: unit as any, element: element as any });
+    export function Location(step: Step, isLower: boolean) {
+        return DataLocation(DataTag, { step, isLower }, {});
     }
 
     export function isLocation(x: any): x is Location {
-        return !!x && x.kind === 'data-location' && x.tag === 'pyramid';
+        return !!x && x.kind === 'data-location' && x.tag === DataTag;
+    }
+
+    export interface Loci extends DataLoci<HalfPyramid, {}> {}
+
+    export function Loci(data: HalfPyramid, elements: ReadonlyArray<{}>): Loci {
+        return DataLoci(DataTag, data, elements, undefined, () => confalPyramidLabel(data));
     }
 
-    export function toElementLocation(location: Location) {
-        return StructureElement.Location.create(location.element.structure, location.element.unit, location.element.element);
+    export function isLoci(x: any): x is Loci {
+        return !!x && x.kind === 'data-loci' && x.tag === DataTag;
     }
 }
diff --git a/src/extensions/dnatco/confal-pyramids/util.ts b/src/extensions/dnatco/confal-pyramids/util.ts
index 467734af416ccba7b34fdf65e64b3e80abf30535..af4e9793f259d202a52ccb0b896c7156bea87ae4 100644
--- a/src/extensions/dnatco/confal-pyramids/util.ts
+++ b/src/extensions/dnatco/confal-pyramids/util.ts
@@ -8,288 +8,120 @@
 import { ConfalPyramidsProvider } from './property';
 import { ConfalPyramidsTypes as CPT } from './types';
 import { Segmentation } from '../../../mol-data/int';
-import { Vec3 } from '../../../mol-math/linear-algebra';
 import { ChainIndex, ElementIndex, ResidueIndex, Structure, StructureElement, StructureProperties, Unit } from '../../../mol-model/structure';
 
-export namespace ConfalPyramidsUtil {
-    type Residue = Segmentation.Segment<ResidueIndex>;
+type Residue = Segmentation.Segment<ResidueIndex>;
 
-    export type AtomInfo = {
-        pos: Vec3,
-        index: ElementIndex,
-        fakeAltId: string,
-    };
+export type Pyramid = {
+    O3: ElementIndex,
+    P: ElementIndex,
+    OP1: ElementIndex,
+    OP2: ElementIndex,
+    O5: ElementIndex,
+    confalScore: number,
+    stepIdx: number,
+};
 
-    export type FirstResidueAtoms = {
-        O3: AtomInfo,
-    };
+const EmptyStepIndices = new Array<number>();
 
-    export type SecondResidueAtoms = {
-        OP1: AtomInfo,
-        OP2: AtomInfo,
-        O5: AtomInfo,
-        P: AtomInfo,
-    };
-
-    type ResidueInfo = {
-        PDB_model_num: number,
-        asym_id: string,
-        auth_asym_id: string,
-        seq_id: number,
-        auth_seq_id: number,
-        comp_id: string,
-        alt_id: string,
-        ins_code: string,
-    };
+function copyResidue(r?: Residue) {
+    return r ? { index: r.index, start: r.start, end: r.end } : void 0;
+}
 
-    export type Handler = (pyramid: CPT.Pyramid, first: FirstResidueAtoms, second: SecondResidueAtoms, firstLocIndex: number, secondLocIndex: number) => void;
+function getAtomIndex(loc: StructureElement.Location, residue: Residue, names: string[], altId: string): ElementIndex {
+    for (let eI = residue.start; eI < residue.end; eI++) {
+        loc.element = loc.unit.elements[eI];
+        const elName = StructureProperties.atom.label_atom_id(loc);
+        const elAltId = StructureProperties.atom.label_alt_id(loc);
 
-    function residueInfoFromLocation(loc: StructureElement.Location): ResidueInfo {
-        return {
-            PDB_model_num: StructureProperties.unit.model_num(loc),
-            asym_id: StructureProperties.chain.label_asym_id(loc),
-            auth_asym_id: StructureProperties.chain.auth_asym_id(loc),
-            seq_id: StructureProperties.residue.label_seq_id(loc),
-            auth_seq_id: StructureProperties.residue.auth_seq_id(loc),
-            comp_id: StructureProperties.atom.label_comp_id(loc),
-            alt_id: StructureProperties.atom.label_alt_id(loc),
-            ins_code: StructureProperties.residue.pdbx_PDB_ins_code(loc)
-        };
+        if (names.includes(elName) && (elAltId === altId || elAltId.length === 0))
+            return loc.element;
     }
 
-    export function hasMultipleModels(unit: Unit.Atomic): boolean {
-        const prop = ConfalPyramidsProvider.get(unit.model).value;
-        if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
-        return prop.data.hasMultipleModels;
-    }
+    return -1 as ElementIndex;
+}
 
-    function getPossibleAltIds(residue: Residue, structure: Structure, unit: Unit.Atomic): string[] {
-        const possibleAltIds: string[] = [];
+function getPyramid(loc: StructureElement.Location, one: Residue, two: Residue, altIdOne: string, altIdTwo: string, confalScore: number, stepIdx: number): Pyramid {
+    const O3 = getAtomIndex(loc, one, ['O3\'', 'O3*'], altIdOne);
+    const P = getAtomIndex(loc, two, ['P'], altIdTwo);
+    const OP1 = getAtomIndex(loc, two, ['OP1'], altIdTwo);
+    const OP2 = getAtomIndex(loc, two, ['OP2'], altIdTwo);
+    const O5 = getAtomIndex(loc, two, ['O5\'', 'O5*'], altIdTwo);
 
-        const loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
-        for (let rI = residue.start; rI <= residue.end - 1; rI++) {
-            loc.element = unit.elements[rI];
-            const altId = StructureProperties.atom.label_alt_id(loc);
-            if (altId !== '' && !possibleAltIds.includes(altId)) possibleAltIds.push(altId);
-        }
+    return { O3, P, OP1, OP2, O5, confalScore, stepIdx };
+}
 
-        return possibleAltIds;
+export class ConfalPyramidsIterator {
+    private chainIt: Segmentation.SegmentIterator<ChainIndex>;
+    private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
+    private residueOne?: Residue;
+    private residueTwo: Residue;
+    private data?: CPT.Steps;
+    private loc: StructureElement.Location;
+
+    private getStepIndices(r: Residue) {
+        this.loc.element = this.loc.unit.elements[r.start];
+
+        const modelIdx = StructureProperties.unit.model_num(this.loc) - 1;
+        const chainId = StructureProperties.chain.auth_asym_id(this.loc);
+        const seqId = StructureProperties.residue.auth_seq_id(this.loc);
+
+        const chains = this.data!.mapping[modelIdx];
+        if (!chains) return EmptyStepIndices;
+        const residues = chains.get(chainId);
+        if (!residues) return EmptyStepIndices;
+        return residues.get(seqId) ?? EmptyStepIndices;
     }
 
-    class Utility {
-        protected getPyramidByName(name: string): { pyramid: CPT.Pyramid | undefined, index: number } {
-            const index = this.data.names.get(name);
-            if (index === undefined) return { pyramid: undefined, index: -1 };
-
-            return { pyramid: this.data.pyramids[index], index };
-        }
-
-        protected stepToName(entry_id: string, modelNum: number, locFirst: StructureElement.Location, locSecond: StructureElement.Location, fakeAltId_1: string, fakeAltId_2: string) {
-            const first = residueInfoFromLocation(locFirst);
-            const second = residueInfoFromLocation(locSecond);
-            const model_id = this.hasMultipleModels ? `-m${modelNum}` : '';
-            const alt_id_1 = fakeAltId_1 !== '' ? `.${fakeAltId_1}` : (first.alt_id.length ? `.${first.alt_id}` : '');
-            const alt_id_2 = fakeAltId_2 !== '' ? `.${fakeAltId_2}` : (second.alt_id.length ? `.${second.alt_id}` : '');
-            const ins_code_1 = first.ins_code.length ? `.${first.ins_code}` : '';
-            const ins_code_2 = second.ins_code.length ? `.${second.ins_code}` : '';
-
-            return `${entry_id}${model_id}_${first.auth_asym_id}_${first.comp_id}${alt_id_1}_${first.auth_seq_id}${ins_code_1}_${second.comp_id}${alt_id_2}_${second.auth_seq_id}${ins_code_2}`;
-        }
-
-        constructor(unit: Unit.Atomic) {
-            const prop = ConfalPyramidsProvider.get(unit.model).value;
-            if (prop === undefined || prop.data === undefined) throw new Error('No custom properties data');
-
-            this.data = prop.data;
-            this.hasMultipleModels = hasMultipleModels(unit);
+    private moveStep() {
+        this.residueOne = copyResidue(this.residueTwo);
+        this.residueTwo = copyResidue(this.residueIt.move())!;
 
-            this.entryId = unit.model.entryId.toLowerCase();
-            this.modelNum = unit.model.modelNum;
-        }
-
-        protected readonly data: CPT.PyramidsData;
-        protected readonly hasMultipleModels: boolean;
-        protected readonly entryId: string;
-        protected readonly modelNum: number;
+        return this.toPyramids(this.residueOne!, this.residueTwo);
     }
 
-    export class UnitWalker extends Utility {
-        private getAtomIndices(names: string[], residue: Residue): ElementIndex[] {
-            const indices: ElementIndex[] = [];
-
-            const loc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            for (let rI = residue.start; rI <= residue.end - 1; rI++) {
-                loc.element = this.unit.elements[rI];
-                const thisName = StructureProperties.atom.label_atom_id(loc);
-                if (names.includes(thisName)) indices.push(loc.element);
-            }
-
-            if (indices.length === 0) {
-                let namesStr = '';
-                for (const n of names)
-                    namesStr += `${n} `;
-
-                throw new Error(`Element [${namesStr}] not found on residue ${residue.index}`);
-            }
-
-            return indices;
-        }
-
-        private getAtomPositions(indices: ElementIndex[]): Vec3[] {
-            const pos = this.unit.conformation.invariantPosition;
-            const positions: Vec3[] = [];
-
-            for (const eI of indices) {
-                const v = Vec3.zero();
-                pos(eI, v);
-                positions.push(v);
-            }
-
-            return positions;
-        }
-
-        private handleStep(firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[]) {
-            const modelNum = this.hasMultipleModels ? this.modelNum : -1;
-            let ok = false;
-
-            const firstLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            const secondLoc = StructureElement.Location.create(this.structure, this.unit, -1 as ElementIndex);
-            for (let i = 0; i < firstAtoms.length; i++) {
-                const first = firstAtoms[i];
-                for (let j = 0; j < secondAtoms.length; j++) {
-                    const second = secondAtoms[j];
-                    firstLoc.element = first.O3.index;
-                    secondLoc.element = second.OP1.index;
+    private toPyramids(one: Residue, two: Residue) {
+        const indices = this.getStepIndices(one);
 
-                    const name = this.stepToName(this.entryId, modelNum, firstLoc, secondLoc, first.O3.fakeAltId, second.OP1.fakeAltId);
-                    const { pyramid, index } = this.getPyramidByName(name);
-                    if (pyramid !== undefined) {
-                        const setLoc = (loc: CPT.Location, eI: ElementIndex) => {
-                            loc.element.structure = this.structure;
-                            loc.element.unit = this.unit;
-                            loc.element.element = eI;
-                        };
-
-                        const locIndex = index * 2;
-                        setLoc(this.data.locations[locIndex], firstLoc.element);
-                        setLoc(this.data.locations[locIndex + 1], secondLoc.element);
-                        this.handler(pyramid, first, second, locIndex, locIndex + 1);
-                        ok = true;
-                    }
-                }
-            }
-
-            if (!ok) throw new Error('Bogus step');
-        }
-
-        private processFirstResidue(residue: Residue, possibleAltIds: string[]) {
-            const indO3 = this.getAtomIndices(['O3\'', 'O3*'], residue);
-            const posO3 = this.getAtomPositions(indO3);
-
-            const altPos: FirstResidueAtoms[] = [
-                { O3: { pos: posO3[0], index: indO3[0], fakeAltId: '' } }
-            ];
-
-            for (let i = 1; i < indO3.length; i++) {
-                altPos.push({ O3: { pos: posO3[i], index: indO3[i], fakeAltId: '' } });
-            }
-
-            if (altPos.length === 1 && possibleAltIds.length > 1) {
-                /* We have some alternate positions on the residue but O3 does not have any - fake them */
-                altPos[0].O3.fakeAltId = possibleAltIds[0];
-
-                for (let i = 1; i < possibleAltIds.length; i++)
-                    altPos.push({ O3: { pos: posO3[0], index: indO3[0], fakeAltId: possibleAltIds[i] } });
-            }
-
-            return altPos;
+        const points = [];
+        for (const idx of indices) {
+            const step = this.data!.steps[idx];
+            points.push(getPyramid(this.loc, one, two, step.label_alt_id_1, step.label_alt_id_2, step.confal_score, idx));
         }
 
-        private processSecondResidue(residue: Residue, possibleAltIds: string[]) {
-            const indOP1 = this.getAtomIndices(['OP1'], residue);
-            const indOP2 = this.getAtomIndices(['OP2'], residue);
-            const indO5 = this.getAtomIndices(['O5\'', 'O5*'], residue);
-            const indP = this.getAtomIndices(['P'], residue);
-
-            const posOP1 = this.getAtomPositions(indOP1);
-            const posOP2 = this.getAtomPositions(indOP2);
-            const posO5 = this.getAtomPositions(indO5);
-            const posP = this.getAtomPositions(indP);
-
-            const infoOP1: AtomInfo[] = [];
-            /* We use OP1 as "pivotal" atom. There is no specific reason
-             * to pick OP1, it is as good a choice as any other atom
-             */
-            if (indOP1.length === 1 && possibleAltIds.length > 1) {
-                /* No altIds on OP1, fake them */
-                for (const altId of possibleAltIds)
-                    infoOP1.push({ pos: posOP1[0], index: indOP1[0], fakeAltId: altId });
-            } else {
-                for (let i = 0; i < indOP1.length; i++)
-                    infoOP1.push({ pos: posOP1[i], index: indOP1[i], fakeAltId: '' });
-            }
-
-            const mkInfo = (i: number, indices: ElementIndex[], positions: Vec3[], altId: string) => {
-                if (i >= indices.length) {
-                    const last = indices.length - 1;
-                    return { pos: positions[last], index: indices[last], fakeAltId: altId };
-                }
-
-                return { pos: positions[i], index: indices[i], fakeAltId: altId };
-            };
-
-            const altPos: SecondResidueAtoms[] = [];
-            for (let i = 0; i < infoOP1.length; i++) {
-                const altId = infoOP1[i].fakeAltId;
-
-                const OP2 = mkInfo(i, indOP2, posOP2, altId);
-                const O5 = mkInfo(i, indO5, posO5, altId);
-                const P = mkInfo(i, indP, posP, altId);
-
-                altPos.push({ OP1: infoOP1[i], OP2, O5, P });
-            }
-
-            return altPos;
-        }
-
-        private step(residue: Residue): { firstAtoms: FirstResidueAtoms[], secondAtoms: SecondResidueAtoms[] } {
-            const firstPossibleAltIds = getPossibleAltIds(residue, this.structure, this.unit);
-            const firstAtoms = this.processFirstResidue(residue, firstPossibleAltIds);
+        return points;
+    }
 
-            residue = this.residueIt.move();
+    constructor(structure: Structure, unit: Unit) {
+        this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
+        this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
 
-            const secondPossibleAltIds = getPossibleAltIds(residue, this.structure, this.unit);
-            const secondAtoms = this.processSecondResidue(residue, secondPossibleAltIds);
+        const prop = ConfalPyramidsProvider.get(unit.model).value;
+        this.data = prop?.data;
 
-            return { firstAtoms, secondAtoms };
+        if (this.chainIt.hasNext) {
+            this.residueIt.setSegment(this.chainIt.move());
+            if (this.residueIt.hasNext)
+                this.residueTwo = this.residueIt.move();
         }
 
-        walk() {
-            while (this.chainIt.hasNext) {
-                this.residueIt.setSegment(this.chainIt.move());
-
-                let residue = this.residueIt.move();
-                while (this.residueIt.hasNext) {
-                    try {
-                        const { firstAtoms, secondAtoms } = this.step(residue);
-
-                        this.handleStep(firstAtoms, secondAtoms);
-                    } catch (error) {
-                        /* Skip and move along */
-                        residue = this.residueIt.move();
-                    }
-                }
-            }
-        }
+        this.loc = StructureElement.Location.create(structure, unit, -1 as ElementIndex);
+    }
 
-        constructor(private structure: Structure, private unit: Unit.Atomic, private handler: Handler) {
-            super(unit);
+    get hasNext() {
+        if (!this.data)
+            return false;
+        return this.residueIt.hasNext
+            ? true
+            : this.chainIt.hasNext;
+    }
 
-            this.chainIt = Segmentation.transientSegments(unit.model.atomicHierarchy.chainAtomSegments, unit.elements);
-            this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements);
+    move() {
+        if (this.residueIt.hasNext) {
+            return this.moveStep();
+        } else {
+            this.residueIt.setSegment(this.chainIt.move());
+            return this.moveStep();
         }
-
-        private chainIt: Segmentation.SegmentIterator<ChainIndex>;
-        private residueIt: Segmentation.SegmentIterator<ResidueIndex>;
     }
 }
diff --git a/src/extensions/geo-export/ui.tsx b/src/extensions/geo-export/ui.tsx
index 837c59021f5fb38fa342a315846ccd3eb8277466..78aa038e85dd6dcd5c6433940c2ea3f6a4e300c0 100644
--- a/src/extensions/geo-export/ui.tsx
+++ b/src/extensions/geo-export/ui.tsx
@@ -60,6 +60,8 @@ export class GeometryExporterUI extends CollapsableControls<{}, State> {
     }
 
     componentDidMount() {
+        if (!this.plugin.canvas3d) return;
+
         const merged = merge(
             this.controls.behaviors.params,
             this.plugin.canvas3d!.reprCount
diff --git a/src/extensions/mp4-export/controls.ts b/src/extensions/mp4-export/controls.ts
index a8edd32816a919b64d0ce1337dc6e6084d2ed5bd..b6dadcc5e80f79a5f6a78201e6cde93c389e2012 100644
--- a/src/extensions/mp4-export/controls.ts
+++ b/src/extensions/mp4-export/controls.ts
@@ -118,11 +118,13 @@ export class Mp4Controls extends PluginComponent {
     }
 
     private init() {
+        if (!this.plugin.canvas3d) return;
+
         this.subscribe(this.plugin.managers.animation.events.updated.pipe(debounceTime(16)), () => {
             this.sync();
         });
 
-        this.subscribe(this.plugin.canvas3d?.resized!, () => this.syncInfo());
+        this.subscribe(this.plugin.canvas3d.resized, () => this.syncInfo());
         this.subscribe(this.plugin.helpers.viewportScreenshot?.events.previewed!, () => this.syncInfo());
 
         this.subscribe(this.plugin.behaviors.state.isBusy, b => this.updateCanApply(b));
diff --git a/src/extensions/mp4-export/encoder.ts b/src/extensions/mp4-export/encoder.ts
index 5dc12af421808abee78e69e341f7b4d7ae263e4f..38e96599220a377c94704939d9a951be46d1ff17 100644
--- a/src/extensions/mp4-export/encoder.ts
+++ b/src/extensions/mp4-export/encoder.ts
@@ -69,6 +69,7 @@ export async function encodeMp4Animation<A extends PluginStateAnimation>(plugin:
         const dt = durationMs / N;
 
         await ctx.update({ message: 'Rendering...', isIndeterminate: false, current: 0, max: N + 1 });
+        await params.pass.updateBackground();
 
         await plugin.managers.animation.play(params.animation.definition, params.animation.params);
         stoppedAnimation = false;
diff --git a/src/extensions/rcsb/graphql/types.ts b/src/extensions/rcsb/graphql/types.ts
index a113cbbe299abb84df59e65f4ee6d21cf6894ea2..58fc86f4e00d4ef5bf0743dbc2e23756466d3edf 100644
--- a/src/extensions/rcsb/graphql/types.ts
+++ b/src/extensions/rcsb/graphql/types.ts
@@ -4,7 +4,7 @@ export type InputMaybe<T> = Maybe<T>;
 export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
 export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
 export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
-// Generated on 2022-06-26T14:02:35-07:00
+// Generated on 2022-08-20T16:36:05-07:00
 
 /** All built-in and custom scalars, mapped to their actual values */
 export type Scalars = {
@@ -13,11 +13,8 @@ export type Scalars = {
   Boolean: boolean;
   Int: number;
   Float: number;
-  /** Built-in scalar representing an instant in time */
   Date: any;
-  /** Built-in scalar for dynamic values */
   ObjectScalar: any;
-  /** Use SPQR's SchemaPrinter to remove this from SDL */
   UNREPRESENTABLE: any;
 };
 
diff --git a/src/mol-canvas3d/camera.ts b/src/mol-canvas3d/camera.ts
index 2b82c50df22dd208c232a961817a14dc06d11247..169478586e24ba8d072321be640a24eccfb887e2 100644
--- a/src/mol-canvas3d/camera.ts
+++ b/src/mol-canvas3d/camera.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -260,7 +260,8 @@ namespace Camera {
             radius: 0,
             radiusMax: 10,
             fog: 50,
-            clipFar: true
+            clipFar: true,
+            minNear: 5,
         };
     }
 
@@ -276,6 +277,7 @@ namespace Camera {
         radiusMax: number
         fog: number
         clipFar: boolean
+        minNear: number
     }
 
     export function copySnapshot(out: Snapshot, source?: Partial<Snapshot>) {
@@ -292,6 +294,7 @@ namespace Camera {
         if (typeof source.radiusMax !== 'undefined') out.radiusMax = source.radiusMax;
         if (typeof source.fog !== 'undefined') out.fog = source.fog;
         if (typeof source.clipFar !== 'undefined') out.clipFar = source.clipFar;
+        if (typeof source.minNear !== 'undefined') out.minNear = source.minNear;
 
         return out;
     }
@@ -303,6 +306,7 @@ namespace Camera {
             && a.radiusMax === b.radiusMax
             && a.fog === b.fog
             && a.clipFar === b.clipFar
+            && a.minNear === b.minNear
             && Vec3.exactEquals(a.position, b.position)
             && Vec3.exactEquals(a.up, b.up)
             && Vec3.exactEquals(a.target, b.target);
@@ -370,7 +374,7 @@ function updatePers(camera: Camera) {
 }
 
 function updateClip(camera: Camera) {
-    let { radius, radiusMax, mode, fog, clipFar } = camera.state;
+    let { radius, radiusMax, mode, fog, clipFar, minNear } = camera.state;
     if (radius < 0.01) radius = 0.01;
 
     const normalizedFar = clipFar ? radius : radiusMax;
@@ -384,12 +388,12 @@ function updateClip(camera: Camera) {
 
     if (mode === 'perspective') {
         // set at least to 5 to avoid slow sphere impostor rendering
-        near = Math.max(Math.min(radiusMax, 5), near);
-        far = Math.max(5, far);
+        near = Math.max(Math.min(radiusMax, minNear), near);
+        far = Math.max(minNear, far);
     } else {
         // not too close to 0 as it causes issues with outline rendering
-        near = Math.max(Math.min(radiusMax, 5), near);
-        far = Math.max(5, far);
+        near = Math.max(Math.min(radiusMax, minNear), near);
+        far = Math.max(minNear, far);
     }
 
     if (near === far) {
diff --git a/src/mol-canvas3d/camera/stereo.ts b/src/mol-canvas3d/camera/stereo.ts
index 0111427191417c5e81137c60bff2f1144b38b560..4742dd93f1bdb4d8b69e6c866af6be6e7d3e4d44 100644
--- a/src/mol-canvas3d/camera/stereo.ts
+++ b/src/mol-canvas3d/camera/stereo.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -13,8 +13,8 @@ import { Camera, ICamera } from '../camera';
 import { Viewport } from './util';
 
 export const StereoCameraParams = {
-    eyeSeparation: PD.Numeric(0.064, { min: 0.01, max: 0.5, step: 0.001 }),
-    focus: PD.Numeric(10, { min: 1, max: 100, step: 0.01 }),
+    eyeSeparation: PD.Numeric(0.062, { min: 0.02, max: 0.1, step: 0.001 }, { description: 'Distance between left and right camera.' }),
+    focus: PD.Numeric(10, { min: 1, max: 20, step: 0.1 }, { description: 'Apparent object distance.' }),
 };
 export const DefaultStereoCameraProps = PD.getDefaultValues(StereoCameraParams);
 export type StereoCameraProps = PD.Values<typeof StereoCameraParams>
diff --git a/src/mol-canvas3d/canvas3d.ts b/src/mol-canvas3d/canvas3d.ts
index 2af6cf0a438d2c36b3585a12660eab8207c619ef..ed05d3a357ba32df5d8d88e57981a2b654d5c205 100644
--- a/src/mol-canvas3d/canvas3d.ts
+++ b/src/mol-canvas3d/canvas3d.ts
@@ -3,6 +3,7 @@
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { BehaviorSubject, Subscription } from 'rxjs';
@@ -39,7 +40,10 @@ import { Helper } from './helper/helper';
 import { Passes } from './passes/passes';
 import { shallowEqual } from '../mol-util';
 import { MarkingParams } from './passes/marking';
-import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit } from '../mol-gl/webgl/render-item';
+import { GraphicsRenderVariantsBlended, GraphicsRenderVariantsWboit, GraphicsRenderVariantsDpoit } from '../mol-gl/webgl/render-item';
+import { degToRad, radToDeg } from '../mol-math/misc';
+import { AssetManager } from '../mol-util/assets';
+import { deepClone } from '../mol-util/object';
 
 export const Canvas3DParams = {
     camera: PD.Group({
@@ -49,6 +53,7 @@ export const Canvas3DParams = {
             on: PD.Group(StereoCameraParams),
             off: PD.Group({})
         }, { cycle: true, hideIf: p => p?.mode !== 'perspective' }),
+        fov: PD.Numeric(45, { min: 10, max: 130, step: 1 }, { label: 'Field of View' }),
         manualReset: PD.Boolean(false, { isHidden: true }),
     }, { pivot: 'mode' }),
     cameraFog: PD.MappedStatic('on', {
@@ -60,6 +65,7 @@ export const Canvas3DParams = {
     cameraClipping: PD.Group({
         radius: PD.Numeric(100, { min: 0, max: 99, step: 1 }, { label: 'Clipping', description: 'How much of the scene to show.' }),
         far: PD.Boolean(true, { description: 'Hide scene in the distance' }),
+        minNear: PD.Numeric(5, { min: 0.1, max: 10, step: 0.1 }, { description: 'Note, may cause performance issues rendering impostors when set too small and cause issues with outline rendering when too close to 0.' }),
     }, { pivot: 'radius' }),
     viewport: PD.MappedStatic('canvas', {
         canvas: PD.Group({}),
@@ -78,7 +84,9 @@ export const Canvas3DParams = {
     }),
 
     cameraResetDurationMs: PD.Numeric(250, { min: 0, max: 1000, step: 1 }, { description: 'The time it takes to reset the camera.' }),
+    sceneRadiusFactor: PD.Numeric(1, { min: 1, max: 10, step: 0.1 }),
     transparentBackground: PD.Boolean(false),
+    dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
 
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
@@ -106,11 +114,13 @@ interface Canvas3DContext {
     readonly attribs: Readonly<Canvas3DContext.Attribs>
     readonly contextLost: BehaviorSubject<now.Timestamp>
     readonly contextRestored: BehaviorSubject<now.Timestamp>
+    readonly assetManager: AssetManager
     dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => void
 }
 
 namespace Canvas3DContext {
     export const DefaultAttribs = {
+        failIfMajorPerformanceCaveat: false,
         /** true by default to avoid issues with Safari (Jan 2021) */
         antialias: true,
         /** true to support multiple Canvas3D objects with a single context */
@@ -120,14 +130,19 @@ namespace Canvas3DContext {
         /** extra pixels to around target to check in case target is empty */
         pickPadding: 1,
         enableWboit: true,
+        enableDpoit: false,
         preferWebGl1: false
     };
     export type Attribs = typeof DefaultAttribs
 
-    export function fromCanvas(canvas: HTMLCanvasElement, attribs: Partial<Attribs> = {}): Canvas3DContext {
+    export function fromCanvas(canvas: HTMLCanvasElement, assetManager: AssetManager, attribs: Partial<Attribs> = {}): Canvas3DContext {
         const a = { ...DefaultAttribs, ...attribs };
-        const { antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
+
+        if (a.enableWboit && a.enableDpoit) throw new Error('Multiple transparency methods not allowed.');
+
+        const { failIfMajorPerformanceCaveat, antialias, preserveDrawingBuffer, pixelScale, preferWebGl1 } = a;
         const gl = getGLContext(canvas, {
+            failIfMajorPerformanceCaveat,
             antialias,
             preserveDrawingBuffer,
             alpha: true, // the renderer requires an alpha channel
@@ -139,7 +154,7 @@ namespace Canvas3DContext {
 
         const input = InputObserver.fromElement(canvas, { pixelScale, preventGestures: true });
         const webgl = createContext(gl, { pixelScale });
-        const passes = new Passes(webgl, attribs);
+        const passes = new Passes(webgl, assetManager, a);
 
         if (isDebugMode) {
             const loseContextExt = gl.getExtension('WEBGL_lose_context');
@@ -192,6 +207,7 @@ namespace Canvas3DContext {
             attribs: a,
             contextLost,
             contextRestored: webgl.contextRestored,
+            assetManager,
             dispose: (options?: Partial<{ doNotForceWebGLContextLoss: boolean }>) => {
                 input.dispose();
 
@@ -278,8 +294,8 @@ namespace Canvas3D {
     export interface DragEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, pageStart: Vec2, pageEnd: Vec2 }
     export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, button: ButtonsType.Flag, modifiers: ModifiersKeys, page?: Vec2, position?: Vec3 }
 
-    export function create({ webgl, input, passes, attribs }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
-        const p: Canvas3DProps = { ...DefaultCanvas3DParams, ...props };
+    export function create({ webgl, input, passes, attribs, assetManager }: Canvas3DContext, props: Partial<Canvas3DProps> = {}): Canvas3D {
+        const p: Canvas3DProps = { ...deepClone(DefaultCanvas3DParams), ...deepClone(props) };
 
         const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>();
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>();
@@ -296,14 +312,19 @@ namespace Canvas3D {
         let width = 128;
         let height = 128;
         updateViewport();
+        const scene = Scene.create(webgl, passes.draw.dpoitEnabled ? GraphicsRenderVariantsDpoit : (passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended));
 
-        const scene = Scene.create(webgl, passes.draw.wboitEnabled ? GraphicsRenderVariantsWboit : GraphicsRenderVariantsBlended);
+        function getSceneRadius() {
+            return scene.boundingSphere.radius * p.sceneRadiusFactor;
+        }
 
         const camera = new Camera({
             position: Vec3.create(0, 0, 100),
             mode: p.camera.mode,
             fog: p.cameraFog.name === 'on' ? p.cameraFog.params.intensity : 0,
-            clipFar: p.cameraClipping.far
+            clipFar: p.cameraClipping.far,
+            minNear: p.cameraClipping.minNear,
+            fov: degToRad(p.camera.fov),
         }, { x, y, width, height }, { pixelScale: attribs.pixelScale });
         const stereoCamera = new StereoCamera(camera, p.camera.stereo.params);
 
@@ -315,6 +336,10 @@ namespace Canvas3D {
         const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input, camera, p.interaction);
         const multiSampleHelper = new MultiSampleHelper(passes.multiSample);
 
+        passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
+            if (changed) requestDraw();
+        });
+
         let cameraResetRequested = false;
         let nextCameraResetDuration: number | undefined = void 0;
         let nextCameraResetSnapshot: Camera.SnapshotProvider | undefined = void 0;
@@ -395,7 +420,7 @@ namespace Canvas3D {
                 y > gl.drawingBufferHeight || y + height < 0
             ) return false;
 
-            const markingUpdated = resolveMarking();
+            const markingUpdated = resolveMarking() && (renderer.props.colorMarker || p.marking.enabled);
 
             let didRender = false;
             controls.update(currentTime);
@@ -523,7 +548,7 @@ namespace Canvas3D {
                 const focus = camera.getFocus(center, radius);
                 const next = typeof nextCameraResetSnapshot === 'function' ? nextCameraResetSnapshot(scene, camera) : nextCameraResetSnapshot;
                 const snapshot = next ? { ...focus, ...next } : focus;
-                camera.setState({ ...snapshot, radiusMax: scene.boundingSphere.radius }, duration);
+                camera.setState({ ...snapshot, radiusMax: getSceneRadius() }, duration);
             }
 
             nextCameraResetDuration = void 0;
@@ -574,7 +599,7 @@ namespace Canvas3D {
             }
             if (oldBoundingSphereVisible.radius === 0) nextCameraResetDuration = 0;
 
-            camera.setState({ radiusMax: scene.boundingSphere.radius }, 0);
+            if (!p.camera.manualReset) camera.setState({ radiusMax: getSceneRadius() }, 0);
             reprCount.next(reprRenderObjects.size);
             if (isDebugMode) consoleStats();
 
@@ -650,7 +675,7 @@ namespace Canvas3D {
 
         function getProps(): Canvas3DProps {
             const radius = scene.boundingSphere.radius > 0
-                ? 100 - Math.round((camera.transition.target.radius / scene.boundingSphere.radius) * 100)
+                ? 100 - Math.round((camera.transition.target.radius / getSceneRadius()) * 100)
                 : 0;
 
             return {
@@ -658,14 +683,17 @@ namespace Canvas3D {
                     mode: camera.state.mode,
                     helper: { ...helper.camera.props },
                     stereo: { ...p.camera.stereo },
+                    fov: Math.round(radToDeg(camera.state.fov)),
                     manualReset: !!p.camera.manualReset
                 },
                 cameraFog: camera.state.fog > 0
                     ? { name: 'on' as const, params: { intensity: camera.state.fog } }
                     : { name: 'off' as const, params: {} },
-                cameraClipping: { far: camera.state.clipFar, radius },
+                cameraClipping: { far: camera.state.clipFar, radius, minNear: camera.state.minNear },
                 cameraResetDurationMs: p.cameraResetDurationMs,
+                sceneRadiusFactor: p.sceneRadiusFactor,
                 transparentBackground: p.transparentBackground,
+                dpoitIterations: p.dpoitIterations,
                 viewport: p.viewport,
 
                 postprocessing: { ...p.postprocessing },
@@ -767,10 +795,19 @@ namespace Canvas3D {
                     ? produce(getProps(), properties as any)
                     : properties;
 
+                if (props.sceneRadiusFactor !== undefined) {
+                    p.sceneRadiusFactor = props.sceneRadiusFactor;
+                    camera.setState({ radiusMax: getSceneRadius() }, 0);
+                }
+
                 const cameraState: Partial<Camera.Snapshot> = Object.create(null);
                 if (props.camera && props.camera.mode !== undefined && props.camera.mode !== camera.state.mode) {
                     cameraState.mode = props.camera.mode;
                 }
+                const oldFov = Math.round(radToDeg(camera.state.fov));
+                if (props.camera && props.camera.fov !== undefined && props.camera.fov !== oldFov) {
+                    cameraState.fov = degToRad(props.camera.fov);
+                }
                 if (props.cameraFog !== undefined && props.cameraFog.params) {
                     const newFog = props.cameraFog.name === 'on' ? props.cameraFog.params.intensity : 0;
                     if (newFog !== camera.state.fog) cameraState.fog = newFog;
@@ -779,8 +816,11 @@ namespace Canvas3D {
                     if (props.cameraClipping.far !== undefined && props.cameraClipping.far !== camera.state.clipFar) {
                         cameraState.clipFar = props.cameraClipping.far;
                     }
+                    if (props.cameraClipping.minNear !== undefined && props.cameraClipping.minNear !== camera.state.minNear) {
+                        cameraState.minNear = props.cameraClipping.minNear;
+                    }
                     if (props.cameraClipping.radius !== undefined) {
-                        const radius = (scene.boundingSphere.radius / 100) * (100 - props.cameraClipping.radius);
+                        const radius = (getSceneRadius() / 100) * (100 - props.cameraClipping.radius);
                         if (radius > 0 && radius !== cameraState.radius) {
                             // if radius = 0, NaNs happen
                             cameraState.radius = Math.max(radius, 0.01);
@@ -791,9 +831,13 @@ namespace Canvas3D {
 
                 if (props.camera?.helper) helper.camera.setProps(props.camera.helper);
                 if (props.camera?.manualReset !== undefined) p.camera.manualReset = props.camera.manualReset;
-                if (props.camera?.stereo !== undefined) Object.assign(p.camera.stereo, props.camera.stereo);
+                if (props.camera?.stereo !== undefined) {
+                    Object.assign(p.camera.stereo, props.camera.stereo);
+                    stereoCamera.setProps(p.camera.stereo.params);
+                }
                 if (props.cameraResetDurationMs !== undefined) p.cameraResetDurationMs = props.cameraResetDurationMs;
                 if (props.transparentBackground !== undefined) p.transparentBackground = props.transparentBackground;
+                if (props.dpoitIterations !== undefined) p.dpoitIterations = props.dpoitIterations;
                 if (props.viewport !== undefined) {
                     const doNotUpdate = p.viewport === props.viewport ||
                         (p.viewport.name === props.viewport.name && shallowEqual(p.viewport.params, props.viewport.params));
@@ -805,6 +849,12 @@ namespace Canvas3D {
                     }
                 }
 
+                if (props.postprocessing?.background) {
+                    Object.assign(p.postprocessing.background, props.postprocessing.background);
+                    passes.draw.postprocessing.background.update(camera, p.postprocessing.background, changed => {
+                        if (changed && !doNotRequestDraw) requestDraw();
+                    });
+                }
                 if (props.postprocessing) Object.assign(p.postprocessing, props.postprocessing);
                 if (props.marking) Object.assign(p.marking, props.marking);
                 if (props.multiSample) Object.assign(p.multiSample, props.multiSample);
@@ -823,7 +873,7 @@ namespace Canvas3D {
                 }
             },
             getImagePass: (props: Partial<ImageProps> = {}) => {
-                return new ImagePass(webgl, renderer, scene, camera, helper, passes.draw.wboitEnabled, props);
+                return new ImagePass(webgl, assetManager, renderer, scene, camera, helper, passes.draw.wboitEnabled, passes.draw.dpoitEnabled, props);
             },
             getRenderObjects(): GraphicsRenderObject[] {
                 const renderObjects: GraphicsRenderObject[] = [];
@@ -888,4 +938,4 @@ namespace Canvas3D {
             Viewport.set(controls.viewport, x, y, width, height);
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-canvas3d/passes/background.ts b/src/mol-canvas3d/passes/background.ts
new file mode 100644
index 0000000000000000000000000000000000000000..14d194b6baa418f1e381cf47b182735590df2be2
--- /dev/null
+++ b/src/mol-canvas3d/passes/background.ts
@@ -0,0 +1,467 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { QuadPositions, } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { AttributeSpec, DefineSpec, TextureSpec, UniformSpec, Values, ValueSpec } from '../../mol-gl/renderable/schema';
+import { ShaderCode } from '../../mol-gl/shader-code';
+import { background_frag } from '../../mol-gl/shader/background.frag';
+import { background_vert } from '../../mol-gl/shader/background.vert';
+import { WebGLContext } from '../../mol-gl/webgl/context';
+import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
+import { createNullTexture, CubeFaces, Texture } from '../../mol-gl/webgl/texture';
+import { Mat4 } from '../../mol-math/linear-algebra/3d/mat4';
+import { ValueCell } from '../../mol-util/value-cell';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { isTimingMode } from '../../mol-util/debug';
+import { Camera, ICamera } from '../camera';
+import { Vec3 } from '../../mol-math/linear-algebra/3d/vec3';
+import { Vec2 } from '../../mol-math/linear-algebra/3d/vec2';
+import { Color } from '../../mol-util/color';
+import { Asset, AssetManager } from '../../mol-util/assets';
+import { Vec4 } from '../../mol-math/linear-algebra/3d/vec4';
+
+const SharedParams = {
+    opacity: PD.Numeric(1, { min: 0.0, max: 1.0, step: 0.01 }),
+    saturation: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }),
+    lightness: PD.Numeric(0, { min: -1, max: 1, step: 0.01 }),
+};
+
+const SkyboxParams = {
+    faces: PD.MappedStatic('urls', {
+        urls: PD.Group({
+            nx: PD.Text('', { label: 'Negative X / Left' }),
+            ny: PD.Text('', { label: 'Negative Y / Bottom' }),
+            nz: PD.Text('', { label: 'Negative Z / Back' }),
+            px: PD.Text('', { label: 'Positive X / Right' }),
+            py: PD.Text('', { label: 'Positive Y / Top' }),
+            pz: PD.Text('', { label: 'Positive Z / Front' }),
+        }, { isExpanded: true, label: 'URLs' }),
+        files: PD.Group({
+            nx: PD.File({ label: 'Negative X / Left', accept: 'image/*' }),
+            ny: PD.File({ label: 'Negative Y / Bottom', accept: 'image/*' }),
+            nz: PD.File({ label: 'Negative Z / Back', accept: 'image/*' }),
+            px: PD.File({ label: 'Positive X / Right', accept: 'image/*' }),
+            py: PD.File({ label: 'Positive Y / Top', accept: 'image/*' }),
+            pz: PD.File({ label: 'Positive Z / Front', accept: 'image/*' }),
+        }, { isExpanded: true, label: 'Files' }),
+    }),
+    blur: PD.Numeric(0, { min: 0.0, max: 1.0, step: 0.01 }, { description: 'Note, this only works in WebGL2 or when "EXT_shader_texture_lod" is available.' }),
+    ...SharedParams,
+};
+type SkyboxProps = PD.Values<typeof SkyboxParams>
+
+const ImageParams = {
+    source: PD.MappedStatic('url', {
+        url: PD.Text(''),
+        file: PD.File({ accept: 'image/*' }),
+    }),
+    ...SharedParams,
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+type ImageProps = PD.Values<typeof ImageParams>
+
+const HorizontalGradientParams = {
+    topColor: PD.Color(Color(0xDDDDDD)),
+    bottomColor: PD.Color(Color(0xEEEEEE)),
+    ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+
+const RadialGradientParams = {
+    centerColor: PD.Color(Color(0xDDDDDD)),
+    edgeColor: PD.Color(Color(0xEEEEEE)),
+    ratio: PD.Numeric(0.5, { min: 0.0, max: 1.0, step: 0.01 }),
+    coverage: PD.Select('viewport', PD.arrayToOptions(['viewport', 'canvas'])),
+};
+
+export const BackgroundParams = {
+    variant: PD.MappedStatic('off', {
+        off: PD.EmptyGroup(),
+        skybox: PD.Group(SkyboxParams, { isExpanded: true }),
+        image: PD.Group(ImageParams, { isExpanded: true }),
+        horizontalGradient: PD.Group(HorizontalGradientParams, { isExpanded: true }),
+        radialGradient: PD.Group(RadialGradientParams, { isExpanded: true }),
+    }, { label: 'Environment' }),
+};
+export type BackgroundProps = PD.Values<typeof BackgroundParams>
+
+export class BackgroundPass {
+    private renderable: BackgroundRenderable;
+
+    private skybox: {
+        texture: Texture
+        props: SkyboxProps
+        assets: Asset[]
+        loaded: boolean
+    } | undefined;
+
+    private image: {
+        texture: Texture
+        props: ImageProps
+        asset: Asset
+        loaded: boolean
+    } | undefined;
+
+    private readonly camera = new Camera();
+    private readonly target = Vec3();
+    private readonly position = Vec3();
+    private readonly dir = Vec3();
+
+    readonly texture: Texture;
+
+    constructor(private readonly webgl: WebGLContext, private readonly assetManager: AssetManager, width: number, height: number) {
+        this.renderable = getBackgroundRenderable(webgl, width, height);
+    }
+
+    setSize(width: number, height: number) {
+        const [w, h] = this.renderable.values.uTexSize.ref.value;
+
+        if (width !== w || height !== h) {
+            ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
+        }
+    }
+
+    private clearSkybox() {
+        if (this.skybox !== undefined) {
+            this.skybox.texture.destroy();
+            this.skybox.assets.forEach(a => this.assetManager.release(a));
+            this.skybox = undefined;
+        }
+    }
+
+    private updateSkybox(camera: ICamera, props: SkyboxProps, onload?: (changed: boolean) => void) {
+        const tf = this.skybox?.props.faces;
+        const f = props.faces.params;
+        if (!f.nx || !f.ny || !f.nz || !f.px || !f.py || !f.pz) {
+            this.clearSkybox();
+            onload?.(false);
+            return;
+        }
+        if (!this.skybox || !tf || !areSkyboxTexturePropsEqual(props.faces, this.skybox.props.faces)) {
+            this.clearSkybox();
+            const { texture, assets } = getSkyboxTexture(this.webgl, this.assetManager, props.faces, errored => {
+                if (this.skybox) this.skybox.loaded = !errored;
+                onload?.(true);
+            });
+            this.skybox = { texture, props: { ...props }, assets, loaded: false };
+            ValueCell.update(this.renderable.values.tSkybox, texture);
+            this.renderable.update();
+        } else {
+            onload?.(false);
+        }
+        if (!this.skybox) return;
+
+        let cam = camera;
+        if (camera.state.mode === 'orthographic') {
+            this.camera.setState({ ...camera.state, mode: 'perspective' });
+            this.camera.update();
+            cam = this.camera;
+        }
+
+        const m = this.renderable.values.uViewDirectionProjectionInverse.ref.value;
+        Vec3.sub(this.dir, cam.state.position, cam.state.target);
+        Vec3.setMagnitude(this.dir, this.dir, 0.1);
+        Vec3.copy(this.position, this.dir);
+        Mat4.lookAt(m, this.position, this.target, cam.state.up);
+        Mat4.mul(m, cam.projection, m);
+        Mat4.invert(m, m);
+        ValueCell.update(this.renderable.values.uViewDirectionProjectionInverse, m);
+
+        ValueCell.updateIfChanged(this.renderable.values.uBlur, props.blur);
+        ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
+        ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
+        ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, 'skybox');
+        this.renderable.update();
+    }
+
+    private clearImage() {
+        if (this.image !== undefined) {
+            this.image.texture.destroy();
+            this.assetManager.release(this.image.asset);
+            this.image = undefined;
+        }
+    }
+
+    private updateImage(props: ImageProps, onload?: (loaded: boolean) => void) {
+        if (!props.source.params) {
+            this.clearImage();
+            onload?.(false);
+            return;
+        }
+        if (!this.image || !this.image.props.source.params || !areImageTexturePropsEqual(props.source, this.image.props.source)) {
+            this.clearImage();
+            const { texture, asset } = getImageTexture(this.webgl, this.assetManager, props.source, errored => {
+                if (this.image) this.image.loaded = !errored;
+                onload?.(true);
+            });
+            this.image = { texture, props: { ...props }, asset, loaded: false };
+            ValueCell.update(this.renderable.values.tImage, texture);
+            this.renderable.update();
+        } else {
+            onload?.(false);
+        }
+        if (!this.image) return;
+
+        ValueCell.updateIfChanged(this.renderable.values.uOpacity, props.opacity);
+        ValueCell.updateIfChanged(this.renderable.values.uSaturation, props.saturation);
+        ValueCell.updateIfChanged(this.renderable.values.uLightness, props.lightness);
+        ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, props.coverage === 'viewport' ? true : false);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, 'image');
+        this.renderable.update();
+    }
+
+    private updateImageScaling() {
+        const v = this.renderable.values;
+        const [w, h] = v.uTexSize.ref.value;
+        const iw = this.image?.texture.getWidth() || 0;
+        const ih = this.image?.texture.getHeight() || 0;
+        const r = w / h;
+        const ir = iw / ih;
+        // responsive scaling with offset
+        if (r < ir) {
+            ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, iw * h / ih, h));
+        } else {
+            ValueCell.update(v.uImageScale, Vec2.set(v.uImageScale.ref.value, w, ih * w / iw));
+        }
+        const [rw, rh] = v.uImageScale.ref.value;
+        const sr = rw / rh;
+        if (sr > r) {
+            ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, (1 - r / sr) / 2, 0));
+        } else {
+            ValueCell.update(v.uImageOffset, Vec2.set(v.uImageOffset.ref.value, 0, (1 - sr / r) / 2));
+        }
+    }
+
+    private updateGradient(colorA: Color, colorB: Color, ratio: number, variant: 'horizontalGradient' | 'radialGradient', viewportAdjusted: boolean) {
+        ValueCell.update(this.renderable.values.uGradientColorA, Color.toVec3Normalized(this.renderable.values.uGradientColorA.ref.value, colorA));
+        ValueCell.update(this.renderable.values.uGradientColorB, Color.toVec3Normalized(this.renderable.values.uGradientColorB.ref.value, colorB));
+        ValueCell.updateIfChanged(this.renderable.values.uGradientRatio, ratio);
+        ValueCell.updateIfChanged(this.renderable.values.uViewportAdjusted, viewportAdjusted);
+        ValueCell.updateIfChanged(this.renderable.values.dVariant, variant);
+        this.renderable.update();
+    }
+
+    update(camera: ICamera, props: BackgroundProps, onload?: (changed: boolean) => void) {
+        if (props.variant.name === 'off') {
+            this.clearSkybox();
+            this.clearImage();
+            onload?.(false);
+            return;
+        } else if (props.variant.name === 'skybox') {
+            this.clearImage();
+            this.updateSkybox(camera, props.variant.params, onload);
+        } else if (props.variant.name === 'image') {
+            this.clearSkybox();
+            this.updateImage(props.variant.params, onload);
+        } else if (props.variant.name === 'horizontalGradient') {
+            this.clearSkybox();
+            this.clearImage();
+            this.updateGradient(props.variant.params.topColor, props.variant.params.bottomColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false);
+            onload?.(false);
+        } else if (props.variant.name === 'radialGradient') {
+            this.clearSkybox();
+            this.clearImage();
+            this.updateGradient(props.variant.params.centerColor, props.variant.params.edgeColor, props.variant.params.ratio, props.variant.name, props.variant.params.coverage === 'viewport' ? true : false);
+            onload?.(false);
+        }
+
+        const { x, y, width, height } = camera.viewport;
+        ValueCell.update(this.renderable.values.uViewport, Vec4.set(this.renderable.values.uViewport.ref.value, x, y, width, height));
+    }
+
+    isEnabled(props: BackgroundProps) {
+        return !!(
+            (this.skybox && this.skybox.loaded) ||
+            (this.image && this.image.loaded) ||
+            props.variant.name === 'horizontalGradient' ||
+            props.variant.name === 'radialGradient'
+        );
+    }
+
+    private isReady() {
+        return !!(
+            (this.skybox && this.skybox.loaded) ||
+            (this.image && this.image.loaded) ||
+            this.renderable.values.dVariant.ref.value === 'horizontalGradient' ||
+            this.renderable.values.dVariant.ref.value === 'radialGradient'
+        );
+    }
+
+    render() {
+        if (!this.isReady()) return;
+
+        if (this.renderable.values.dVariant.ref.value === 'image') {
+            this.updateImageScaling();
+        }
+
+        if (isTimingMode) this.webgl.timer.mark('BackgroundPass.render');
+        this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('BackgroundPass.render');
+    }
+
+    dispose() {
+        this.clearSkybox();
+        this.clearImage();
+    }
+}
+
+//
+
+const SkyboxName = 'background-skybox';
+
+type CubeAssets = { [k in keyof CubeFaces]: Asset };
+
+function getCubeAssets(assetManager: AssetManager, faces: SkyboxProps['faces']): CubeAssets {
+    if (faces.name === 'urls') {
+        return {
+            nx: Asset.getUrlAsset(assetManager, faces.params.nx),
+            ny: Asset.getUrlAsset(assetManager, faces.params.ny),
+            nz: Asset.getUrlAsset(assetManager, faces.params.nz),
+            px: Asset.getUrlAsset(assetManager, faces.params.px),
+            py: Asset.getUrlAsset(assetManager, faces.params.py),
+            pz: Asset.getUrlAsset(assetManager, faces.params.pz),
+        };
+    } else {
+        return {
+            nx: faces.params.nx!,
+            ny: faces.params.ny!,
+            nz: faces.params.nz!,
+            px: faces.params.px!,
+            py: faces.params.py!,
+            pz: faces.params.pz!,
+        };
+    }
+}
+
+function getCubeFaces(assetManager: AssetManager, cubeAssets: CubeAssets): CubeFaces {
+    const resolve = (asset: Asset) => {
+        return assetManager.resolve(asset, 'binary').run().then(a => new Blob([a.data]));
+    };
+
+    return {
+        nx: resolve(cubeAssets.nx),
+        ny: resolve(cubeAssets.ny),
+        nz: resolve(cubeAssets.nz),
+        px: resolve(cubeAssets.px),
+        py: resolve(cubeAssets.py),
+        pz: resolve(cubeAssets.pz),
+    };
+}
+
+function getSkyboxHash(faces: SkyboxProps['faces']) {
+    if (faces.name === 'urls') {
+        return `${SkyboxName}_${faces.params.nx}|${faces.params.ny}|${faces.params.nz}|${faces.params.px}|${faces.params.py}|${faces.params.pz}`;
+    } else {
+        return `${SkyboxName}_${faces.params.nx?.id}|${faces.params.ny?.id}|${faces.params.nz?.id}|${faces.params.px?.id}|${faces.params.py?.id}|${faces.params.pz?.id}`;
+    }
+}
+
+function areSkyboxTexturePropsEqual(facesA: SkyboxProps['faces'], facesB: SkyboxProps['faces']) {
+    return getSkyboxHash(facesA) === getSkyboxHash(facesB);
+}
+
+function getSkyboxTexture(ctx: WebGLContext, assetManager: AssetManager, faces: SkyboxProps['faces'], onload?: (errored?: boolean) => void): { texture: Texture, assets: Asset[] } {
+    const cubeAssets = getCubeAssets(assetManager, faces);
+    const cubeFaces = getCubeFaces(assetManager, cubeAssets);
+    const assets = [cubeAssets.nx, cubeAssets.ny, cubeAssets.nz, cubeAssets.px, cubeAssets.py, cubeAssets.pz];
+    const texture = ctx.resources.cubeTexture(cubeFaces, true, onload);
+    return { texture, assets };
+}
+
+//
+
+const ImageName = 'background-image';
+
+function getImageHash(source: ImageProps['source']) {
+    if (source.name === 'url') {
+        return `${ImageName}_${source.params}`;
+    } else {
+        return `${ImageName}_${source.params?.id}`;
+    }
+}
+
+function areImageTexturePropsEqual(sourceA: ImageProps['source'], sourceB: ImageProps['source']) {
+    return getImageHash(sourceA) === getImageHash(sourceB);
+}
+
+function getImageTexture(ctx: WebGLContext, assetManager: AssetManager, source: ImageProps['source'], onload?: (errored?: boolean) => void): { texture: Texture, asset: Asset } {
+    const texture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'linear');
+    const img = new Image();
+    img.onload = () => {
+        texture.load(img);
+        onload?.();
+    };
+    img.onerror = () => {
+        onload?.(true);
+    };
+    const asset = source.name === 'url'
+        ? Asset.getUrlAsset(assetManager, source.params)
+        : source.params!;
+    assetManager.resolve(asset, 'binary').run().then(a => {
+        const blob = new Blob([a.data]);
+        img.src = URL.createObjectURL(blob);
+    });
+    return { texture, asset };
+}
+
+//
+
+const BackgroundSchema = {
+    drawCount: ValueSpec('number'),
+    instanceCount: ValueSpec('number'),
+    aPosition: AttributeSpec('float32', 2, 0),
+    tSkybox: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    tImage: TextureSpec('texture', 'rgba', 'ubyte', 'linear'),
+    uImageScale: UniformSpec('v2'),
+    uImageOffset: UniformSpec('v2'),
+    uTexSize: UniformSpec('v2'),
+    uViewport: UniformSpec('v4'),
+    uViewportAdjusted: UniformSpec('b'),
+    uViewDirectionProjectionInverse: UniformSpec('m4'),
+    uGradientColorA: UniformSpec('v3'),
+    uGradientColorB: UniformSpec('v3'),
+    uGradientRatio: UniformSpec('f'),
+    uBlur: UniformSpec('f'),
+    uOpacity: UniformSpec('f'),
+    uSaturation: UniformSpec('f'),
+    uLightness: UniformSpec('f'),
+    dVariant: DefineSpec('string', ['skybox', 'image', 'verticalGradient', 'horizontalGradient', 'radialGradient']),
+};
+const SkyboxShaderCode = ShaderCode('background', background_vert, background_frag, {
+    shaderTextureLod: 'optional'
+});
+type BackgroundRenderable = ComputeRenderable<Values<typeof BackgroundSchema>>
+
+function getBackgroundRenderable(ctx: WebGLContext, width: number, height: number): BackgroundRenderable {
+    const values: Values<typeof BackgroundSchema> = {
+        drawCount: ValueCell.create(6),
+        instanceCount: ValueCell.create(1),
+        aPosition: ValueCell.create(QuadPositions),
+        tSkybox: ValueCell.create(createNullTexture()),
+        tImage: ValueCell.create(createNullTexture()),
+        uImageScale: ValueCell.create(Vec2()),
+        uImageOffset: ValueCell.create(Vec2()),
+        uTexSize: ValueCell.create(Vec2.create(width, height)),
+        uViewport: ValueCell.create(Vec4()),
+        uViewportAdjusted: ValueCell.create(true),
+        uViewDirectionProjectionInverse: ValueCell.create(Mat4()),
+        uGradientColorA: ValueCell.create(Vec3()),
+        uGradientColorB: ValueCell.create(Vec3()),
+        uGradientRatio: ValueCell.create(0.5),
+        uBlur: ValueCell.create(0),
+        uOpacity: ValueCell.create(1),
+        uSaturation: ValueCell.create(0),
+        uLightness: ValueCell.create(0),
+        dVariant: ValueCell.create('skybox'),
+    };
+
+    const schema = { ...BackgroundSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', SkyboxShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
diff --git a/src/mol-canvas3d/passes/dpoit.ts b/src/mol-canvas3d/passes/dpoit.ts
new file mode 100644
index 0000000000000000000000000000000000000000..634d4ec658a4c532dc693ffcbdbeee3ef5682d7c
--- /dev/null
+++ b/src/mol-canvas3d/passes/dpoit.ts
@@ -0,0 +1,309 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * Adapted from https://github.com/tsherif/webgl2examples, The MIT License, Copyright © 2017 Tarek Sherif, Shuai Shao
+ */
+
+import { QuadSchema, QuadValues } from '../../mol-gl/compute/util';
+import { ComputeRenderable, createComputeRenderable } from '../../mol-gl/renderable';
+import { TextureSpec, UniformSpec, 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 } from '../../mol-gl/webgl/texture';
+import { ValueCell } from '../../mol-util';
+import { quad_vert } from '../../mol-gl/shader/quad.vert';
+import { evaluateDpoit_frag } from '../../mol-gl/shader/evaluate-dpoit.frag';
+import { blendBackDpoit_frag } from '../../mol-gl/shader/blend-back-dpoit.frag';
+import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
+import { Vec2 } from '../../mol-math/linear-algebra';
+import { isDebugMode, isTimingMode } from '../../mol-util/debug';
+import { isWebGL2 } from '../../mol-gl/webgl/compat';
+
+const BlendBackDpoitSchema = {
+    ...QuadSchema,
+    tDpoitBackColor: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const BlendBackDpoitShaderCode = ShaderCode('blend-back-dpoit', quad_vert, blendBackDpoit_frag);
+type BlendBackDpoitRenderable = ComputeRenderable<Values<typeof BlendBackDpoitSchema>>
+
+function getBlendBackDpoitRenderable(ctx: WebGLContext, dopitBlendBackTexture: Texture): BlendBackDpoitRenderable {
+    const values: Values<typeof BlendBackDpoitSchema> = {
+        ...QuadValues,
+        tDpoitBackColor: ValueCell.create(dopitBlendBackTexture),
+        uTexSize: ValueCell.create(Vec2.create(dopitBlendBackTexture.getWidth(), dopitBlendBackTexture.getHeight())),
+    };
+
+    const schema = { ...BlendBackDpoitSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', BlendBackDpoitShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+const EvaluateDpoitSchema = {
+    ...QuadSchema,
+    tDpoitFrontColor: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uTexSize: UniformSpec('v2'),
+};
+const EvaluateDpoitShaderCode = ShaderCode('evaluate-dpoit', quad_vert, evaluateDpoit_frag);
+type EvaluateDpoitRenderable = ComputeRenderable<Values<typeof EvaluateDpoitSchema>>
+
+function getEvaluateDpoitRenderable(ctx: WebGLContext, dpoitFrontColorTexture: Texture): EvaluateDpoitRenderable {
+    const values: Values<typeof EvaluateDpoitSchema> = {
+        ...QuadValues,
+        tDpoitFrontColor: ValueCell.create(dpoitFrontColorTexture),
+        uTexSize: ValueCell.create(Vec2.create(dpoitFrontColorTexture.getWidth(), dpoitFrontColorTexture.getHeight())),
+    };
+
+    const schema = { ...EvaluateDpoitSchema };
+    const renderItem = createComputeRenderItem(ctx, 'triangles', EvaluateDpoitShaderCode, schema, values);
+
+    return createComputeRenderable(renderItem, values);
+}
+
+export class DpoitPass {
+    private readonly DEPTH_CLEAR_VALUE = -99999.0; // NOTE same constant is set in shaders
+    private readonly MAX_DEPTH = 1.0;
+    private readonly MIN_DEPTH = 0.0;
+
+    private passCount = 0;
+    private writeId: number;
+    private readId: number;
+
+    private readonly blendBackRenderable: BlendBackDpoitRenderable;
+    private readonly renderable: EvaluateDpoitRenderable;
+
+    private readonly depthFramebuffers: Framebuffer[];
+    private readonly colorFramebuffers: Framebuffer[];
+
+    private readonly depthTextures: Texture[];
+    private readonly colorFrontTextures: Texture[];
+    private readonly colorBackTextures: Texture[];
+
+    private _supported = false;
+    get supported() {
+        return this._supported;
+    }
+
+    bind() {
+        const { state, gl, extensions: { blendMinMax } } = this.webgl;
+
+        // initialize
+        this.passCount = 0;
+
+        this.depthFramebuffers[0].bind();
+        state.clearColor(this.DEPTH_CLEAR_VALUE, this.DEPTH_CLEAR_VALUE, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.depthFramebuffers[1].bind();
+        state.clearColor(-this.MIN_DEPTH, this.MAX_DEPTH, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.colorFramebuffers[0].bind();
+        state.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.colorFramebuffers[1].bind();
+        state.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.depthFramebuffers[0].bind();
+        state.blendEquation(blendMinMax!.MAX);
+        state.depthMask(false);
+
+        return {
+            depth: this.depthTextures[1],
+            frontColor: this.colorFrontTextures[1],
+            backColor: this.colorBackTextures[1]
+        };
+    }
+
+    bindDualDepthPeeling() {
+        const { state, gl, extensions: { blendMinMax } } = this.webgl;
+
+        this.readId = this.passCount % 2;
+        this.writeId = 1 - this.readId; // ping-pong: 0 or 1
+
+        this.passCount += 1; // increment for next pass
+
+        this.depthFramebuffers[this.writeId].bind();
+        state.clearColor(this.DEPTH_CLEAR_VALUE, this.DEPTH_CLEAR_VALUE, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.colorFramebuffers[this.writeId].bind();
+        state.clearColor(0, 0, 0, 0);
+        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        this.depthFramebuffers[this.writeId].bind();
+        state.blendEquation(blendMinMax!.MAX);
+        state.depthMask(false);
+
+        return {
+            depth: this.depthTextures[this.readId],
+            frontColor: this.colorFrontTextures[this.readId],
+            backColor: this.colorBackTextures[this.readId]
+        };
+    }
+
+    renderBlendBack() {
+        if (isTimingMode) this.webgl.timer.mark('DpoitPass.renderBlendBack');
+        const { state, gl } = this.webgl;
+
+        state.blendEquation(gl.FUNC_ADD);
+        state.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        ValueCell.update(this.blendBackRenderable.values.tDpoitBackColor, this.colorBackTextures[this.writeId]);
+
+        this.blendBackRenderable.update();
+        this.blendBackRenderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.renderBlendBack');
+    }
+
+    render() {
+        if (isTimingMode) this.webgl.timer.mark('DpoitPass.render');
+        const { state, gl } = this.webgl;
+
+        state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+
+        ValueCell.update(this.renderable.values.tDpoitFrontColor, this.colorFrontTextures[this.writeId]);
+
+        this.renderable.update();
+        this.renderable.render();
+        if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.render');
+    }
+
+    setSize(width: number, height: number) {
+        const [w, h] = this.renderable.values.uTexSize.ref.value;
+        if (width !== w || height !== h) {
+            for (let i = 0; i < 2; i++) {
+                this.depthTextures[i].define(width, height);
+                this.colorFrontTextures[i].define(width, height);
+                this.colorBackTextures[i].define(width, height);
+            }
+            ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
+            ValueCell.update(this.blendBackRenderable.values.uTexSize, Vec2.set(this.blendBackRenderable.values.uTexSize.ref.value, width, height));
+        }
+    }
+
+    reset() {
+        if (this._supported) this._init();
+    }
+
+    private _init() {
+        const { extensions: { drawBuffers } } = this.webgl;
+        for (let i = 0; i < 2; i++) {
+            // depth
+            this.depthFramebuffers[i].bind();
+            drawBuffers!.drawBuffers([
+                drawBuffers!.COLOR_ATTACHMENT0,
+                drawBuffers!.COLOR_ATTACHMENT1,
+                drawBuffers!.COLOR_ATTACHMENT2
+            ]);
+
+            this.colorFrontTextures[i].attachFramebuffer(this.depthFramebuffers[i], 'color0');
+            this.colorBackTextures[i].attachFramebuffer(this.depthFramebuffers[i], 'color1');
+            this.depthTextures[i].attachFramebuffer(this.depthFramebuffers[i], 'color2');
+
+            // color
+            this.colorFramebuffers[i].bind();
+            drawBuffers!.drawBuffers([
+                drawBuffers!.COLOR_ATTACHMENT0,
+                drawBuffers!.COLOR_ATTACHMENT1
+            ]);
+
+            this.colorFrontTextures[i].attachFramebuffer(this.colorFramebuffers[i], 'color0');
+            this.colorBackTextures[i].attachFramebuffer(this.colorFramebuffers[i], 'color1');
+        }
+    }
+
+    static isSupported(webgl: WebGLContext) {
+        const { extensions: { drawBuffers, textureFloat, colorBufferFloat, blendMinMax } } = webgl;
+        if (!textureFloat || !colorBufferFloat || !drawBuffers || !blendMinMax) {
+            if (isDebugMode) {
+                const missing: string[] = [];
+                if (!textureFloat) missing.push('textureFloat');
+                if (!colorBufferFloat) missing.push('colorBufferFloat');
+                if (!drawBuffers) missing.push('drawBuffers');
+                if (!blendMinMax) missing.push('blendMinMax');
+                console.log(`Missing "${missing.join('", "')}" extensions required for "dpoit"`);
+            }
+            return false;
+        } else {
+            return true;
+        }
+    }
+
+    constructor(private webgl: WebGLContext, width: number, height: number) {
+        if (!DpoitPass.isSupported(webgl)) return;
+
+        const { resources, extensions: { colorBufferHalfFloat, textureHalfFloat } } = webgl;
+
+        // textures
+
+        if (isWebGL2(webgl.gl)) {
+            this.depthTextures = [
+                resources.texture('image-float32', 'rg', 'float', 'nearest'),
+                resources.texture('image-float32', 'rg', 'float', 'nearest')
+            ];
+
+            this.colorFrontTextures = colorBufferHalfFloat && textureHalfFloat ? [
+                resources.texture('image-float16', 'rgba', 'fp16', 'nearest'),
+                resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
+            ] : [
+                resources.texture('image-float32', 'rgba', 'float', 'nearest'),
+                resources.texture('image-float32', 'rgba', 'float', 'nearest')
+            ];
+
+            this.colorBackTextures = colorBufferHalfFloat && textureHalfFloat ? [
+                resources.texture('image-float16', 'rgba', 'fp16', 'nearest'),
+                resources.texture('image-float16', 'rgba', 'fp16', 'nearest')
+            ] : [
+                resources.texture('image-float32', 'rgba', 'float', 'nearest'),
+                resources.texture('image-float32', 'rgba', 'float', 'nearest')
+            ];
+        } else {
+            // in webgl1 drawbuffers must be in the same format for some reason
+
+            this.depthTextures = [
+                resources.texture('image-float32', 'rgba', 'float', 'nearest'),
+                resources.texture('image-float32', 'rgba', 'float', 'nearest')
+            ];
+
+            this.colorFrontTextures = [
+                resources.texture('image-float32', 'rgba', 'float', 'nearest'),
+                resources.texture('image-float32', 'rgba', 'float', 'nearest')
+            ];
+
+            this.colorBackTextures = [
+                resources.texture('image-float32', 'rgba', 'float', 'nearest'),
+                resources.texture('image-float32', 'rgba', 'float', 'nearest')
+            ];
+        }
+
+        this.depthTextures[0].define(width, height);
+        this.depthTextures[1].define(width, height);
+
+        this.colorFrontTextures[0].define(width, height);
+        this.colorFrontTextures[1].define(width, height);
+
+        this.colorBackTextures[0].define(width, height);
+        this.colorBackTextures[1].define(width, height);
+
+        // framebuffers
+
+        this.depthFramebuffers = [resources.framebuffer(), resources.framebuffer()];
+        this.colorFramebuffers = [resources.framebuffer(), resources.framebuffer()];
+
+        // renderables
+
+        this.blendBackRenderable = getBlendBackDpoitRenderable(webgl, this.colorBackTextures[0]);
+        this.renderable = getEvaluateDpoitRenderable(webgl, this.colorFrontTextures[0]);
+
+        this._supported = true;
+        this._init();
+    }
+}
diff --git a/src/mol-canvas3d/passes/draw.ts b/src/mol-canvas3d/passes/draw.ts
index 0bb002d84c9df136b5bb8ce43025f07b8b583652..34282e8408d16a0c59251178c6ab14843914d707 100644
--- a/src/mol-canvas3d/passes/draw.ts
+++ b/src/mol-canvas3d/passes/draw.ts
@@ -3,6 +3,7 @@
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author Áron Samuel Kovács <aron.kovacs@mail.muni.cz>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { WebGLContext } from '../../mol-gl/webgl/context';
@@ -17,15 +18,18 @@ import { Helper } from '../helper/helper';
 
 import { StereoCamera } from '../camera/stereo';
 import { WboitPass } from './wboit';
+import { DpoitPass } from './dpoit';
 import { AntialiasingPass, PostprocessingPass, PostprocessingProps } from './postprocessing';
 import { MarkingPass, MarkingProps } from './marking';
 import { CopyRenderable, createCopyRenderable } from '../../mol-gl/compute/util';
 import { isTimingMode } from '../../mol-util/debug';
+import { AssetManager } from '../../mol-util/assets';
 
 type Props = {
-    postprocessing: PostprocessingProps
-    marking: MarkingProps
+    postprocessing: PostprocessingProps;
+    marking: MarkingProps;
     transparentBackground: boolean;
+    dpoitIterations: number;
 }
 
 type RenderContext = {
@@ -50,7 +54,8 @@ export class DrawPass {
     private copyFboTarget: CopyRenderable;
     private copyFboPostprocessing: CopyRenderable;
 
-    private wboit: WboitPass | undefined;
+    private readonly wboit: WboitPass | undefined;
+    private readonly dpoit: DpoitPass | undefined;
     private readonly marking: MarkingPass;
     readonly postprocessing: PostprocessingPass;
     private readonly antialiasing: AntialiasingPass;
@@ -59,11 +64,13 @@ export class DrawPass {
         return !!this.wboit?.supported;
     }
 
-    constructor(private webgl: WebGLContext, width: number, height: number, enableWboit: boolean) {
-        const { extensions, resources, isWebGL2 } = webgl;
+    get dpoitEnabled() {
+        return !!this.dpoit?.supported;
+    }
 
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, width: number, height: number, enableWboit: boolean, enableDpoit: boolean) {
+        const { extensions, resources, isWebGL2 } = webgl;
         this.drawTarget = createNullRenderTarget(webgl.gl);
-
         this.colorTarget = webgl.createRenderTarget(width, height, true, 'uint8', 'linear');
         this.packedDepth = !extensions.depthTexture;
 
@@ -78,8 +85,9 @@ export class DrawPass {
         }
 
         this.wboit = enableWboit ? new WboitPass(webgl, width, height) : undefined;
+        this.dpoit = enableDpoit ? new DpoitPass(webgl, width, height) : undefined;
         this.marking = new MarkingPass(webgl, width, height);
-        this.postprocessing = new PostprocessingPass(webgl, this);
+        this.postprocessing = new PostprocessingPass(webgl, assetManager, this);
         this.antialiasing = new AntialiasingPass(webgl, this);
 
         this.copyFboTarget = createCopyRenderable(webgl, this.colorTarget.texture);
@@ -88,6 +96,7 @@ export class DrawPass {
 
     reset() {
         this.wboit?.reset();
+        this.dpoit?.reset();
     }
 
     setSize(width: number, height: number) {
@@ -111,23 +120,80 @@ export class DrawPass {
                 this.wboit.setSize(width, height);
             }
 
+            if (this.dpoit?.supported) {
+                this.dpoit.setSize(width, height);
+            }
+
             this.marking.setSize(width, height);
             this.postprocessing.setSize(width, height);
             this.antialiasing.setSize(width, height);
         }
     }
 
+    private _renderDpoit(renderer: Renderer, camera: ICamera, scene: Scene, iterations: number, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
+        if (!this.dpoit?.supported) throw new Error('expected dpoit to be supported');
+
+        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+        renderer.clear(true);
+
+        // render opaque primitives
+        if (scene.hasOpaque) {
+            renderer.renderDpoitOpaque(scene.primitives, camera, null);
+        }
+
+        if (PostprocessingPass.isEnabled(postprocessingProps)) {
+            if (PostprocessingPass.isOutlineEnabled(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);
+        }
+
+        this.depthTextureOpaque.detachFramebuffer(this.colorTarget.framebuffer, 'depth');
+
+        // render transparent primitives
+        if (scene.opacityAverage < 1) {
+            const target = PostprocessingPass.isEnabled(postprocessingProps)
+                ? this.postprocessing.target : this.colorTarget;
+
+            const dpoitTextures = this.dpoit.bind();
+            renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
+
+            for (let i = 0; i < iterations; i++) {
+                if (isTimingMode) this.webgl.timer.mark('DpoitPass.layer');
+                const dpoitTextures = this.dpoit.bindDualDepthPeeling();
+                renderer.renderDpoitTransparent(scene.primitives, camera, this.depthTextureOpaque, dpoitTextures);
+
+                target.bind();
+                this.dpoit.renderBlendBack();
+                if (isTimingMode) this.webgl.timer.markEnd('DpoitPass.layer');
+            }
+
+            // evaluate dpoit
+            target.bind();
+            this.dpoit.render();
+        }
+
+        // render transparent volumes
+        if (scene.volumes.renderables.length > 0) {
+            renderer.renderDpoitVolume(scene.volumes, camera, this.depthTextureOpaque);
+        }
+    }
+
     private _renderWboit(renderer: Renderer, camera: ICamera, scene: Scene, transparentBackground: boolean, postprocessingProps: PostprocessingProps) {
         if (!this.wboit?.supported) throw new Error('expected wboit to be supported');
 
-        this.colorTarget.bind();
+        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
         renderer.clear(true);
 
         // render opaque primitives
-        this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
-        this.colorTarget.bind();
-        renderer.clearDepth();
-        renderer.renderWboitOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderWboitOpaque(scene.primitives, camera, null);
+        }
 
         if (PostprocessingPass.isEnabled(postprocessingProps)) {
             if (PostprocessingPass.isOutlineEnabled(postprocessingProps)) {
@@ -165,14 +231,17 @@ export class DrawPass {
         if (toDrawingBuffer) {
             this.drawTarget.bind();
         } else {
-            this.colorTarget.bind();
             if (!this.packedDepth) {
                 this.depthTextureOpaque.attachFramebuffer(this.colorTarget.framebuffer, 'depth');
+            } else {
+                this.colorTarget.bind();
             }
         }
 
         renderer.clear(true);
-        renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        if (scene.hasOpaque) {
+            renderer.renderBlendedOpaque(scene.primitives, camera, null);
+        }
 
         if (!toDrawingBuffer) {
             // do a depth pass if not rendering to drawing buffer and
@@ -235,7 +304,7 @@ export class DrawPass {
         }
     }
 
-    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, props: Props) {
+    private _render(renderer: Renderer, camera: ICamera, scene: Scene, helper: Helper, toDrawingBuffer: boolean, transparentBackground: boolean, props: Props) {
         const volumeRendering = scene.volumes.renderables.length > 0;
         const postprocessingEnabled = PostprocessingPass.isEnabled(props.postprocessing);
         const antialiasingEnabled = AntialiasingPass.isEnabled(props.postprocessing);
@@ -245,54 +314,54 @@ export class DrawPass {
         renderer.setViewport(x, y, width, height);
         renderer.update(camera);
 
-        if (props.transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
+        if (transparentBackground && !antialiasingEnabled && toDrawingBuffer) {
             this.drawTarget.bind();
             renderer.clear(false);
         }
 
         if (this.wboitEnabled) {
-            this._renderWboit(renderer, camera, scene, props.transparentBackground, props.postprocessing);
+            this._renderWboit(renderer, camera, scene, transparentBackground, props.postprocessing);
+        } else if (this.dpoitEnabled) {
+            this._renderDpoit(renderer, camera, scene, props.dpoitIterations, transparentBackground, props.postprocessing);
         } else {
-            this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, props.transparentBackground, props.postprocessing);
+            this._renderBlended(renderer, camera, scene, !volumeRendering && !postprocessingEnabled && !antialiasingEnabled && toDrawingBuffer, transparentBackground, props.postprocessing);
         }
 
-        if (postprocessingEnabled) {
-            this.postprocessing.target.bind();
-        } else if (!toDrawingBuffer || volumeRendering || this.wboitEnabled) {
-            this.colorTarget.bind();
-        } else {
-            this.drawTarget.bind();
-        }
-
-        if (markingEnabled) {
-            if (scene.markerAverage > 0) {
-                const markingDepthTest = props.marking.ghostEdgeStrength < 1;
-                if (markingDepthTest && scene.markerAverage !== 1) {
-                    this.marking.depthTarget.bind();
-                    renderer.clear(false, true);
-                    renderer.renderMarkingDepth(scene.primitives, camera, null);
-                }
+        const target = postprocessingEnabled
+            ? this.postprocessing.target
+            : !toDrawingBuffer || volumeRendering || this.wboitEnabled || this.dpoitEnabled
+                ? this.colorTarget
+                : this.drawTarget;
 
-                this.marking.maskTarget.bind();
+        if (markingEnabled && scene.markerAverage > 0) {
+            const markingDepthTest = props.marking.ghostEdgeStrength < 1;
+            if (markingDepthTest && scene.markerAverage !== 1) {
+                this.marking.depthTarget.bind();
                 renderer.clear(false, true);
-                renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
-
-                this.marking.update(props.marking);
-                this.marking.render(camera.viewport, postprocessingEnabled ? this.postprocessing.target : this.colorTarget);
+                renderer.renderMarkingDepth(scene.primitives, camera, null);
             }
+
+            this.marking.maskTarget.bind();
+            renderer.clear(false, true);
+            renderer.renderMarkingMask(scene.primitives, camera, markingDepthTest ? this.marking.depthTarget.texture : null);
+
+            this.marking.update(props.marking);
+            this.marking.render(camera.viewport, target);
+        } else {
+            target.bind();
         }
 
         if (helper.debug.isEnabled) {
             helper.debug.syncVisibility();
-            renderer.renderBlended(helper.debug.scene, camera, null);
+            renderer.renderBlended(helper.debug.scene, camera);
         }
         if (helper.handle.isEnabled) {
-            renderer.renderBlended(helper.handle.scene, camera, null);
+            renderer.renderBlended(helper.handle.scene, camera);
         }
         if (helper.camera.isEnabled) {
             helper.camera.update(camera);
             renderer.update(helper.camera.camera);
-            renderer.renderBlended(helper.camera.scene, helper.camera.camera, null);
+            renderer.renderBlended(helper.camera.scene, helper.camera.camera);
         }
 
         if (antialiasingEnabled) {
@@ -303,7 +372,7 @@ export class DrawPass {
             this.webgl.state.disable(this.webgl.gl.DEPTH_TEST);
             if (postprocessingEnabled) {
                 this.copyFboPostprocessing.render();
-            } else if (volumeRendering || this.wboitEnabled) {
+            } else if (volumeRendering || this.wboitEnabled || this.dpoitEnabled) {
                 this.copyFboTarget.render();
             }
         }
@@ -314,15 +383,23 @@ export class DrawPass {
     render(ctx: RenderContext, props: Props, toDrawingBuffer: boolean) {
         if (isTimingMode) this.webgl.timer.mark('DrawPass.render');
         const { renderer, camera, scene, helper } = ctx;
-        renderer.setTransparentBackground(props.transparentBackground);
+
+        this.postprocessing.setTransparentBackground(props.transparentBackground);
+        const transparentBackground = props.transparentBackground || this.postprocessing.background.isEnabled(props.postprocessing.background);
+
+        renderer.setTransparentBackground(transparentBackground);
         renderer.setDrawingBufferSize(this.colorTarget.getWidth(), this.colorTarget.getHeight());
         renderer.setPixelRatio(this.webgl.pixelRatio);
 
         if (StereoCamera.is(camera)) {
-            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, props);
-            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, props);
+            if (isTimingMode) this.webgl.timer.mark('StereoCamera.left');
+            this._render(renderer, camera.left, scene, helper, toDrawingBuffer, transparentBackground, props);
+            if (isTimingMode) this.webgl.timer.markEnd('StereoCamera.left');
+            if (isTimingMode) this.webgl.timer.mark('StereoCamera.right');
+            this._render(renderer, camera.right, scene, helper, toDrawingBuffer, transparentBackground, props);
+            if (isTimingMode) this.webgl.timer.markEnd('StereoCamera.right');
         } else {
-            this._render(renderer, camera, scene, helper, toDrawingBuffer, props);
+            this._render(renderer, camera, scene, helper, toDrawingBuffer, transparentBackground, props);
         }
         if (isTimingMode) this.webgl.timer.markEnd('DrawPass.render');
     }
@@ -335,4 +412,4 @@ export class DrawPass {
         }
         return this.colorTarget;
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-canvas3d/passes/fxaa.ts b/src/mol-canvas3d/passes/fxaa.ts
index ff1a0e878775ca6a898c0983382e10ac834c9091..bbb02430284d72c25be9f93b76bf863970e018ee 100644
--- a/src/mol-canvas3d/passes/fxaa.ts
+++ b/src/mol-canvas3d/passes/fxaa.ts
@@ -44,8 +44,8 @@ export class FxaaPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);
diff --git a/src/mol-canvas3d/passes/image.ts b/src/mol-canvas3d/passes/image.ts
index 68acfa4c4b6977b36669d0b3a7afdbc6b01b96a2..46434fdbdd0e0a7eb43206158a0188aadc20e363 100644
--- a/src/mol-canvas3d/passes/image.ts
+++ b/src/mol-canvas3d/passes/image.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -18,9 +18,11 @@ import { PixelData } from '../../mol-util/image';
 import { Helper } from '../helper/helper';
 import { CameraHelper, CameraHelperParams } from '../helper/camera-helper';
 import { MarkingParams } from './marking';
+import { AssetManager } from '../../mol-util/assets';
 
 export const ImageParams = {
     transparentBackground: PD.Boolean(false),
+    dpoitIterations: PD.Numeric(2, { min: 1, max: 10, step: 1 }),
     multiSample: PD.Group(MultiSampleParams),
     postprocessing: PD.Group(PostprocessingParams),
     marking: PD.Group(MarkingParams),
@@ -47,10 +49,10 @@ export class ImagePass {
     get width() { return this._width; }
     get height() { return this._height; }
 
-    constructor(private webgl: WebGLContext, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, props: Partial<ImageProps>) {
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, private renderer: Renderer, private scene: Scene, private camera: Camera, helper: Helper, enableWboit: boolean, enableDpoit: boolean, props: Partial<ImageProps>) {
         this.props = { ...PD.getDefaultValues(ImageParams), ...props };
 
-        this.drawPass = new DrawPass(webgl, 128, 128, enableWboit);
+        this.drawPass = new DrawPass(webgl, assetManager, 128, 128, enableWboit, enableDpoit);
         this.multiSamplePass = new MultiSamplePass(webgl, this.drawPass);
         this.multiSampleHelper = new MultiSampleHelper(this.multiSamplePass);
 
@@ -63,6 +65,14 @@ export class ImagePass {
         this.setSize(1024, 768);
     }
 
+    updateBackground() {
+        return new Promise<void>(resolve => {
+            this.drawPass.postprocessing.background.update(this.camera, this.props.postprocessing.background, () => {
+                resolve();
+            });
+        });
+    }
+
     setSize(width: number, height: number) {
         if (width === this._width && height === this._height) return;
 
diff --git a/src/mol-canvas3d/passes/marking.ts b/src/mol-canvas3d/passes/marking.ts
index 73cde519fa611d2160c7a2c1788c5120146cf1e4..2093b5f2d1e2d0f818fbdd6519c2021f7654218d 100644
--- a/src/mol-canvas3d/passes/marking.ts
+++ b/src/mol-canvas3d/passes/marking.ts
@@ -64,8 +64,8 @@ export class MarkingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.clearColor(0, 0, 0, 0);
         gl.clear(gl.COLOR_BUFFER_BIT);
@@ -82,8 +82,8 @@ export class MarkingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
 
     setSize(width: number, height: number) {
diff --git a/src/mol-canvas3d/passes/multi-sample.ts b/src/mol-canvas3d/passes/multi-sample.ts
index 82c861372d1c7677531d3861298695e764014da4..cb559ab81ef45666364f37bc4f61cb3e56234435 100644
--- a/src/mol-canvas3d/passes/multi-sample.ts
+++ b/src/mol-canvas3d/passes/multi-sample.ts
@@ -61,6 +61,7 @@ type Props = {
     postprocessing: PostprocessingProps
     marking: MarkingProps
     transparentBackground: boolean;
+    dpoitIterations: number;
 }
 
 type RenderContext = {
@@ -176,8 +177,8 @@ export class MultiSamplePass {
             state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             if (i === 0) {
                 state.clearColor(0, 0, 0, 0);
                 gl.clear(gl.COLOR_BUFFER_BIT);
@@ -192,8 +193,8 @@ export class MultiSamplePass {
         compose.update();
 
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         state.disable(gl.BLEND);
         compose.render();
@@ -231,8 +232,8 @@ export class MultiSamplePass {
             state.disable(gl.BLEND);
             state.disable(gl.DEPTH_TEST);
             state.depthMask(false);
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
             compose.render();
             sampleIndex += 1;
         } else {
@@ -267,8 +268,8 @@ export class MultiSamplePass {
                 state.blendFuncSeparate(gl.ONE, gl.ONE, gl.ONE, gl.ONE);
                 state.disable(gl.DEPTH_TEST);
                 state.depthMask(false);
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (sampleIndex === 0) {
                     state.clearColor(0, 0, 0, 0);
                     gl.clear(gl.COLOR_BUFFER_BIT);
@@ -283,8 +284,8 @@ export class MultiSamplePass {
         drawPass.postprocessing.setOcclusionOffset(0, 0);
 
         this.bindOutputTarget(toDrawingBuffer);
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
         const accumulationWeight = sampleIndex * sampleWeight;
         if (accumulationWeight > 0) {
diff --git a/src/mol-canvas3d/passes/passes.ts b/src/mol-canvas3d/passes/passes.ts
index 208795e33bb2af60d8966f2f857fb87dc594087a..8974562c2e936241bf5a07a859ba40d6ade39d74 100644
--- a/src/mol-canvas3d/passes/passes.ts
+++ b/src/mol-canvas3d/passes/passes.ts
@@ -8,22 +8,26 @@ import { DrawPass } from './draw';
 import { PickPass } from './pick';
 import { MultiSamplePass } from './multi-sample';
 import { WebGLContext } from '../../mol-gl/webgl/context';
+import { AssetManager } from '../../mol-util/assets';
 
 export class Passes {
     readonly draw: DrawPass;
     readonly pick: PickPass;
     readonly multiSample: MultiSamplePass;
 
-    constructor(private webgl: WebGLContext, attribs: Partial<{ pickScale: number, enableWboit: boolean }> = {}) {
+    constructor(private webgl: WebGLContext, assetManager: AssetManager, attribs: Partial<{ pickScale: number, enableWboit: boolean, enableDpoit: boolean }> = {}) {
         const { gl } = webgl;
-        this.draw = new DrawPass(webgl, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false);
+        this.draw = new DrawPass(webgl, assetManager, gl.drawingBufferWidth, gl.drawingBufferHeight, attribs.enableWboit || false, attribs.enableDpoit || false);
         this.pick = new PickPass(webgl, this.draw, attribs.pickScale || 0.25);
         this.multiSample = new MultiSamplePass(webgl, this.draw);
     }
 
     updateSize() {
         const { gl } = this.webgl;
-        this.draw.setSize(gl.drawingBufferWidth, gl.drawingBufferHeight);
+        // Avoid setting dimensions to 0x0 because it causes "empty textures are not allowed" error.
+        const width = Math.max(gl.drawingBufferWidth, 2);
+        const height = Math.max(gl.drawingBufferHeight, 2);
+        this.draw.setSize(width, height);
         this.pick.syncSize();
         this.multiSample.syncSize();
     }
diff --git a/src/mol-canvas3d/passes/postprocessing.ts b/src/mol-canvas3d/passes/postprocessing.ts
index fd41b8abe62071ec6a85734b6db554753e1ed03d..591517976863a47013460eb3158cfeeb699619a4 100644
--- a/src/mol-canvas3d/passes/postprocessing.ts
+++ b/src/mol-canvas3d/passes/postprocessing.ts
@@ -28,6 +28,8 @@ import { Color } from '../../mol-util/color';
 import { FxaaParams, FxaaPass } from './fxaa';
 import { SmaaParams, SmaaPass } from './smaa';
 import { isTimingMode } from '../../mol-util/debug';
+import { BackgroundParams, BackgroundPass } from './background';
+import { AssetManager } from '../../mol-util/assets';
 
 const OutlinesSchema = {
     ...QuadSchema,
@@ -91,7 +93,7 @@ function getSsaoRenderable(ctx: WebGLContext, depthTexture: Texture): SsaoRender
         ...QuadValues,
         tDepth: ValueCell.create(depthTexture),
 
-        uSamples: ValueCell.create([0.0, 0.0, 1.0]),
+        uSamples: ValueCell.create(getSamples(32)),
         dNSamples: ValueCell.create(32),
 
         uProjection: ValueCell.create(Mat4.identity()),
@@ -138,7 +140,7 @@ function getSsaoBlurRenderable(ctx: WebGLContext, ssaoDepthTexture: Texture, dir
         tSsaoDepth: ValueCell.create(ssaoDepthTexture),
         uTexSize: ValueCell.create(Vec2.create(ssaoDepthTexture.getWidth(), ssaoDepthTexture.getHeight())),
 
-        uKernel: ValueCell.create([0.0]),
+        uKernel: ValueCell.create(getBlurKernel(15)),
         dOcclusionKernelSize: ValueCell.create(15),
 
         uBlurDirectionX: ValueCell.create(direction === 'horizontal' ? 1 : 0),
@@ -171,15 +173,26 @@ function getBlurKernel(kernelSize: number): number[] {
     return kernel;
 }
 
-function getSamples(vectorSamples: Vec3[], nSamples: number): number[] {
+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(vectorSamples[i][0] * scale);
-        samples.push(vectorSamples[i][1] * scale);
-        samples.push(vectorSamples[i][2] * scale);
+        samples.push(RandomHemisphereVector[i][0] * scale);
+        samples.push(RandomHemisphereVector[i][1] * scale);
+        samples.push(RandomHemisphereVector[i][2] * scale);
     }
 
     return samples;
@@ -274,12 +287,13 @@ export const PostprocessingParams = {
         smaa: PD.Group(SmaaParams),
         off: PD.Group({})
     }, { options: [['fxaa', 'FXAA'], ['smaa', 'SMAA'], ['off', 'Off']], description: 'Smooth pixel edges' }),
+    background: PD.Group(BackgroundParams, { isFlat: true }),
 };
 export type PostprocessingProps = PD.Values<typeof PostprocessingParams>
 
 export class PostprocessingPass {
     static isEnabled(props: PostprocessingProps) {
-        return props.occlusion.name === 'on' || props.outline.name === 'on';
+        return props.occlusion.name === 'on' || props.outline.name === 'on' || props.background.variant.name !== 'off';
     }
 
     static isOutlineEnabled(props: PostprocessingProps) {
@@ -291,7 +305,6 @@ export class PostprocessingPass {
     private readonly outlinesTarget: RenderTarget;
     private readonly outlinesRenderable: OutlinesRenderable;
 
-    private readonly randomHemisphereVector: Vec3[];
     private readonly ssaoFramebuffer: Framebuffer;
     private readonly ssaoBlurFirstPassFramebuffer: Framebuffer;
     private readonly ssaoBlurSecondPassFramebuffer: Framebuffer;
@@ -318,7 +331,10 @@ export class PostprocessingPass {
         return Math.min(1, 1 / this.webgl.pixelRatio) * this.downsampleFactor;
     }
 
-    constructor(private webgl: WebGLContext, private drawPass: DrawPass) {
+    private readonly bgColor = Vec3();
+    readonly background: BackgroundPass;
+
+    constructor(private readonly webgl: WebGLContext, assetManager: AssetManager, private readonly drawPass: DrawPass) {
         const { colorTarget, depthTextureTransparent, depthTextureOpaque } = drawPass;
         const width = colorTarget.getWidth();
         const height = colorTarget.getHeight();
@@ -334,16 +350,6 @@ export class PostprocessingPass {
         this.outlinesTarget = webgl.createRenderTarget(width, height, false);
         this.outlinesRenderable = getOutlinesRenderable(webgl, depthTextureOpaque, depthTextureTransparent);
 
-        this.randomHemisphereVector = [];
-        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());
-            this.randomHemisphereVector.push(v);
-        }
         this.ssaoFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurFirstPassFramebuffer = webgl.resources.framebuffer();
         this.ssaoBlurSecondPassFramebuffer = webgl.resources.framebuffer();
@@ -368,6 +374,8 @@ export class PostprocessingPass {
         this.ssaoBlurFirstPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthTexture, 'horizontal');
         this.ssaoBlurSecondPassRenderable = getSsaoBlurRenderable(webgl, this.ssaoDepthBlurProxyTexture, 'vertical');
         this.renderable = getPostprocessingRenderable(webgl, colorTarget.texture, depthTextureOpaque, depthTextureTransparent, this.outlinesTarget.texture, this.ssaoDepthTexture);
+
+        this.background = new BackgroundPass(webgl, assetManager, width, height);
     }
 
     setSize(width: number, height: number) {
@@ -391,6 +399,8 @@ export class PostprocessingPass {
             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));
+
+            this.background.setSize(width, height);
         }
     }
 
@@ -440,7 +450,7 @@ export class PostprocessingPass {
                 needsUpdateSsao = true;
 
                 this.nSamples = props.occlusion.params.samples;
-                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.randomHemisphereVector, this.nSamples));
+                ValueCell.update(this.ssaoRenderable.values.uSamples, getSamples(this.nSamples));
                 ValueCell.updateIfChanged(this.ssaoRenderable.values.dNSamples, this.nSamples);
             }
             ValueCell.updateIfChanged(this.ssaoRenderable.values.uRadius, Math.pow(2, props.occlusion.params.radius));
@@ -538,8 +548,8 @@ export class PostprocessingPass {
         state.depthMask(false);
 
         const { x, y, width, height } = camera.viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
     }
 
     private occlusionOffset: [x: number, y: number] = [0, 0];
@@ -549,6 +559,11 @@ export class PostprocessingPass {
         ValueCell.update(this.renderable.values.uOcclusionOffset, Vec2.set(this.renderable.values.uOcclusionOffset.ref.value, x, y));
     }
 
+    private transparentBackground = false;
+    setTransparentBackground(value: boolean) {
+        this.transparentBackground = value;
+    }
+
     render(camera: ICamera, toDrawingBuffer: boolean, transparentBackground: boolean, backgroundColor: Color, props: PostprocessingProps) {
         if (isTimingMode) this.webgl.timer.mark('PostprocessingPass.render');
         this.updateState(camera, transparentBackground, backgroundColor, props);
@@ -583,8 +598,23 @@ export class PostprocessingPass {
         }
 
         const { gl, state } = this.webgl;
-        state.clearColor(0, 0, 0, 1);
-        gl.clear(gl.COLOR_BUFFER_BIT);
+
+        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.renderable.render();
         if (isTimingMode) this.webgl.timer.markEnd('PostprocessingPass.render');
diff --git a/src/mol-canvas3d/passes/smaa.ts b/src/mol-canvas3d/passes/smaa.ts
index 3002b2ff33f3b4bd244675dc264ec4aa49f30c36..79752e4da69232a69081ab6795baf71c7488ba42 100644
--- a/src/mol-canvas3d/passes/smaa.ts
+++ b/src/mol-canvas3d/passes/smaa.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -11,7 +11,7 @@ import { ShaderCode } from '../../mol-gl/shader-code';
 import { WebGLContext } from '../../mol-gl/webgl/context';
 import { createComputeRenderItem } from '../../mol-gl/webgl/render-item';
 import { RenderTarget } from '../../mol-gl/webgl/render-target';
-import { createTexture, loadImageTexture, Texture } from '../../mol-gl/webgl/texture';
+import { loadImageTexture, Texture } from '../../mol-gl/webgl/texture';
 import { Vec2, Vec4 } from '../../mol-math/linear-algebra';
 import { ValueCell } from '../../mol-util';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
@@ -71,9 +71,10 @@ export class SmaaPass {
         state.depthMask(false);
 
         const { x, y, width, height } = viewport;
-        gl.viewport(x, y, width, height);
-        gl.scissor(x, y, width, height);
+        state.viewport(x, y, width, height);
+        state.scissor(x, y, width, height);
 
+        state.colorMask(true, true, true, true);
         state.clearColor(0, 0, 0, 1);
         gl.clear(gl.COLOR_BUFFER_BIT);
 
@@ -191,8 +192,8 @@ function getWeightsRenderable(ctx: WebGLContext, edgesTexture: Texture): Weights
     const width = edgesTexture.getWidth();
     const height = edgesTexture.getHeight();
 
-    const areaTexture = createTexture(ctx.gl, ctx.extensions, 'image-uint8', 'rgb', 'ubyte', 'linear');
-    const searchTexture = createTexture(ctx.gl, ctx.extensions, 'image-uint8', 'rgba', 'ubyte', 'nearest');
+    const areaTexture = ctx.resources.texture('image-uint8', 'rgb', 'ubyte', 'linear');
+    const searchTexture = ctx.resources.texture('image-uint8', 'rgba', 'ubyte', 'nearest');
 
     const values: Values<typeof WeightsSchema> = {
         ...QuadValues,
diff --git a/src/mol-canvas3d/passes/wboit.ts b/src/mol-canvas3d/passes/wboit.ts
index c78f809a1e4b619d866b14557a936bbf4a0ffd15..210a94f51c1efb0ecd5d95a5932b6f2d958e4351 100644
--- a/src/mol-canvas3d/passes/wboit.ts
+++ b/src/mol-canvas3d/passes/wboit.ts
@@ -18,6 +18,8 @@ import { evaluateWboit_frag } from '../../mol-gl/shader/evaluate-wboit.frag';
 import { Framebuffer } from '../../mol-gl/webgl/framebuffer';
 import { Vec2 } from '../../mol-math/linear-algebra';
 import { isDebugMode, isTimingMode } from '../../mol-util/debug';
+import { isWebGL2 } from '../../mol-gl/webgl/compat';
+import { Renderbuffer } from '../../mol-gl/webgl/renderbuffer';
 
 const EvaluateWboitSchema = {
     ...QuadSchema,
@@ -50,6 +52,7 @@ export class WboitPass {
     private readonly framebuffer: Framebuffer;
     private readonly textureA: Texture;
     private readonly textureB: Texture;
+    private readonly depthRenderbuffer: Renderbuffer;
 
     private _supported = false;
     get supported() {
@@ -87,6 +90,7 @@ export class WboitPass {
         if (width !== w || height !== h) {
             this.textureA.define(width, height);
             this.textureB.define(width, height);
+            this.depthRenderbuffer.setSize(width, height);
             ValueCell.update(this.renderable.values.uTexSize, Vec2.set(this.renderable.values.uTexSize.ref.value, width, height));
         }
     }
@@ -106,6 +110,8 @@ export class WboitPass {
 
         this.textureA.attachFramebuffer(this.framebuffer, 'color0');
         this.textureB.attachFramebuffer(this.framebuffer, 'color1');
+
+        this.depthRenderbuffer.attachFramebuffer(this.framebuffer);
     }
 
     static isSupported(webgl: WebGLContext) {
@@ -128,7 +134,7 @@ export class WboitPass {
     constructor(private webgl: WebGLContext, width: number, height: number) {
         if (!WboitPass.isSupported(webgl)) return;
 
-        const { resources } = webgl;
+        const { resources, gl } = webgl;
 
         this.textureA = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureA.define(width, height);
@@ -136,6 +142,10 @@ export class WboitPass {
         this.textureB = resources.texture('image-float32', 'rgba', 'float', 'nearest');
         this.textureB.define(width, height);
 
+        this.depthRenderbuffer = isWebGL2(gl)
+            ? resources.renderbuffer('depth32f', 'depth', width, height)
+            : resources.renderbuffer('depth16', 'depth', width, height);
+
         this.renderable = getEvaluateWboitRenderable(webgl, this.textureA, this.textureB);
         this.framebuffer = resources.framebuffer();
 
diff --git a/src/mol-geo/geometry/base.ts b/src/mol-geo/geometry/base.ts
index ca2ba47d8f0a1a50b974cc56fffd31543e4568bd..d17a47077333819561ef57ceb5234b08b8bee45e 100644
--- a/src/mol-geo/geometry/base.ts
+++ b/src/mol-geo/geometry/base.ts
@@ -111,6 +111,7 @@ export namespace BaseGeometry {
             uRoughness: ValueCell.create(props.material.roughness),
             uBumpiness: ValueCell.create(props.material.bumpiness),
             dLightCount: ValueCell.create(1),
+            dColorMarker: ValueCell.create(true),
 
             dClipObjectCount: ValueCell.create(clip.objects.count),
             dClipVariant: ValueCell.create(clip.variant),
diff --git a/src/mol-geo/geometry/mesh/mesh.ts b/src/mol-geo/geometry/mesh/mesh.ts
index 29b08ba3a935cac0f9a95f70da6e7bcef23f1ad5..10b9c9b058105fa54e6fd2d95f743f36dec8d059 100644
--- a/src/mol-geo/geometry/mesh/mesh.ts
+++ b/src/mol-geo/geometry/mesh/mesh.ts
@@ -45,6 +45,8 @@ export interface Mesh {
     readonly normalBuffer: ValueCell<Float32Array>,
     /** Group buffer as array of group ids for each vertex wrapped in a value cell */
     readonly groupBuffer: ValueCell<Float32Array>,
+    /** Indicates that group may vary within a triangle, wrapped in a value cell */
+    readonly varyingGroup: ValueCell<boolean>,
 
     /** Bounding sphere of the mesh */
     readonly boundingSphere: Sphere3D
@@ -95,6 +97,7 @@ export namespace Mesh {
             indexBuffer: ValueCell.create(indices),
             normalBuffer: ValueCell.create(normals),
             groupBuffer: ValueCell.create(groups),
+            varyingGroup: ValueCell.create(false),
             get boundingSphere() {
                 const newHash = hashCode(mesh);
                 if (newHash !== currentHash) {
@@ -686,6 +689,7 @@ export namespace Mesh {
             aNormal: mesh.normalBuffer,
             aGroup: mesh.groupBuffer,
             elements: mesh.indexBuffer,
+            dVaryingGroup: mesh.varyingGroup,
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
             uInvariantBoundingSphere: ValueCell.create(Vec4.ofSphere(invariantBoundingSphere)),
diff --git a/src/mol-geo/geometry/text/font-atlas.ts b/src/mol-geo/geometry/text/font-atlas.ts
index 609a6985a50ab450799a602ed1ae91caaa95beb3..834b8ecbd49a1f278644496d67946c9554a0501e 100644
--- a/src/mol-geo/geometry/text/font-atlas.ts
+++ b/src/mol-geo/geometry/text/font-atlas.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -88,7 +88,7 @@ export class FontAtlas {
         this.scratchCanvas.width = this.maxWidth;
         this.scratchCanvas.height = this.lineHeight;
 
-        this.scratchContext = this.scratchCanvas.getContext('2d')!;
+        this.scratchContext = this.scratchCanvas.getContext('2d', { willReadFrequently: true })!;
         this.scratchContext.font = `${p.fontStyle} ${p.fontVariant} ${p.fontWeight} ${fontSize}px ${p.fontFamily}`;
         this.scratchContext.fillStyle = 'black';
         this.scratchContext.textBaseline = 'middle';
diff --git a/src/mol-geo/geometry/texture-mesh/color-smoothing.ts b/src/mol-geo/geometry/texture-mesh/color-smoothing.ts
index 7d28455cd9fc98871a2a88be6c6483fdf107b208..8633a1a0577876f532dc4d7c5ae0efe8a8cac1b2 100644
--- a/src/mol-geo/geometry/texture-mesh/color-smoothing.ts
+++ b/src/mol-geo/geometry/texture-mesh/color-smoothing.ts
@@ -27,20 +27,18 @@ export const ColorAccumulateSchema = {
     instanceCount: ValueSpec('number'),
     stride: ValueSpec('number'),
 
-    uTotalCount: UniformSpec('i'),
-    uInstanceCount: UniformSpec('i'),
-    uGroupCount: UniformSpec('i'),
+    uGroupCount: UniformSpec('i', 'material'),
 
     aTransform: AttributeSpec('float32', 16, 1),
     aInstance: AttributeSpec('float32', 1, 1),
     aSample: AttributeSpec('float32', 1, 0),
 
-    uGeoTexDim: UniformSpec('v2', 'buffered'),
-    tPosition: TextureSpec('texture', 'rgba', 'float', 'nearest'),
-    tGroup: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    uGeoTexDim: UniformSpec('v2', 'material'),
+    tPosition: TextureSpec('texture', 'rgba', 'float', 'nearest', 'material'),
+    tGroup: TextureSpec('texture', 'rgba', 'float', 'nearest', 'material'),
 
-    uColorTexDim: UniformSpec('v2'),
-    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest'),
+    uColorTexDim: UniformSpec('v2', 'material'),
+    tColor: TextureSpec('texture', 'rgba', 'ubyte', 'nearest', 'material'),
     dColorType: DefineSpec('string', ['group', 'groupInstance', 'vertex', 'vertexInstance']),
 
     uCurrentSlice: UniformSpec('f'),
@@ -88,8 +86,6 @@ function getAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, box:
         ValueCell.updateIfChanged(v.instanceCount, input.instanceCount);
         ValueCell.updateIfChanged(v.stride, stride);
 
-        ValueCell.updateIfChanged(v.uTotalCount, input.vertexCount);
-        ValueCell.updateIfChanged(v.uInstanceCount, input.instanceCount);
         ValueCell.updateIfChanged(v.uGroupCount, input.groupCount);
 
         ValueCell.update(v.aTransform, input.transformBuffer);
@@ -126,8 +122,6 @@ function createAccumulateRenderable(ctx: WebGLContext, input: AccumulateInput, b
         instanceCount: ValueCell.create(input.instanceCount),
         stride: ValueCell.create(stride),
 
-        uTotalCount: ValueCell.create(input.vertexCount),
-        uInstanceCount: ValueCell.create(input.instanceCount),
         uGroupCount: ValueCell.create(input.groupCount),
 
         aTransform: ValueCell.create(input.transformBuffer),
@@ -325,8 +319,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     if (isTimingMode) webgl.timer.mark('ColorAccumulate.render');
     setAccumulateDefaults(webgl);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     ValueCell.update(uCurrentY, 0);
     let currCol = 0;
@@ -342,8 +336,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
         // console.log({ i, currX, currY });
         ValueCell.update(uCurrentX, currX);
         ValueCell.update(uCurrentSlice, i);
-        gl.viewport(currX, currY, dx, dy);
-        gl.scissor(currX, currY, dx, dy);
+        state.viewport(currX, currY, dx, dy);
+        state.scissor(currX, currY, dx, dy);
         accumulateRenderable.render();
         ++currCol;
         currX += dx;
@@ -377,8 +371,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
 
     setNormalizeDefaults(webgl);
     texture.attachFramebuffer(framebuffer, 0);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     normalizeRenderable.render();
     if (isTimingMode) webgl.timer.markEnd('ColorNormalize.render');
@@ -393,6 +387,8 @@ export function calcTextureMeshColorSmoothing(input: ColorSmoothingInput, resolu
     const type = isInstanceType ? 'volumeInstance' : 'volume';
     if (isTimingMode) webgl.timer.markEnd('calcTextureMeshColorSmoothing');
 
+    // printTextureImage(readTexture(webgl, texture), { scale: 0.75 });
+
     return { texture, gridDim, gridTexDim: Vec2.create(width, height), gridTransform, type };
 }
 
diff --git a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts
index 8c941543972963b7e949c500ee4c0d5c41ab347e..0a246b3e0e29a1b49e1dfec9ef18accccbf7c01b 100644
--- a/src/mol-geo/geometry/texture-mesh/texture-mesh.ts
+++ b/src/mol-geo/geometry/texture-mesh/texture-mesh.ts
@@ -38,6 +38,7 @@ export interface TextureMesh {
     readonly vertexTexture: ValueCell<Texture>,
     readonly groupTexture: ValueCell<Texture>,
     readonly normalTexture: ValueCell<Texture>,
+    readonly varyingGroup: ValueCell<boolean>,
     readonly doubleBuffer: TextureMesh.DoubleBuffer
 
     readonly boundingSphere: Sphere3D
@@ -92,6 +93,7 @@ export namespace TextureMesh {
                 vertexTexture: ValueCell.create(vertexTexture),
                 groupTexture: ValueCell.create(groupTexture),
                 normalTexture: ValueCell.create(normalTexture),
+                varyingGroup: ValueCell.create(false),
                 doubleBuffer: new DoubleBuffer(),
                 boundingSphere: Sphere3D.clone(boundingSphere),
                 meta: {}
@@ -157,6 +159,7 @@ export namespace TextureMesh {
             tPosition: textureMesh.vertexTexture,
             tGroup: textureMesh.groupTexture,
             tNormal: textureMesh.normalTexture,
+            dVaryingGroup: textureMesh.varyingGroup,
 
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
diff --git a/src/mol-gl/_spec/renderer.spec.ts b/src/mol-gl/_spec/renderer.spec.ts
index 7d39b63ae9ac46a24cb51bbd13666aaf001323c2..4842ca9c9407e97059239e3b167f945c3acbbd6f 100644
--- a/src/mol-gl/_spec/renderer.spec.ts
+++ b/src/mol-gl/_spec/renderer.spec.ts
@@ -53,17 +53,17 @@ describe('renderer', () => {
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(ctx.isWebGL2 ? 4 : 5);
         expect(ctx.stats.resourceCounts.texture).toBe(9);
-        expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 5 : 0);
-        expect(ctx.stats.resourceCounts.program).toBe(5);
-        expect(ctx.stats.resourceCounts.shader).toBe(10);
+        expect(ctx.stats.resourceCounts.vertexArray).toBe(ctx.extensions.vertexArrayObject ? 6 : 0);
+        expect(ctx.stats.resourceCounts.program).toBe(6);
+        expect(ctx.stats.resourceCounts.shader).toBe(12);
 
         scene.remove(points);
         scene.commit();
         expect(ctx.stats.resourceCounts.attribute).toBe(0);
         expect(ctx.stats.resourceCounts.texture).toBe(1);
         expect(ctx.stats.resourceCounts.vertexArray).toBe(0);
-        expect(ctx.stats.resourceCounts.program).toBe(5);
-        expect(ctx.stats.resourceCounts.shader).toBe(10);
+        expect(ctx.stats.resourceCounts.program).toBe(6);
+        expect(ctx.stats.resourceCounts.shader).toBe(12);
 
         ctx.resources.destroy();
         expect(ctx.stats.resourceCounts.program).toBe(0);
diff --git a/src/mol-gl/compute/grid3d.ts b/src/mol-gl/compute/grid3d.ts
index e291661ccae12a921b5d127ce6ecdbbc22617ce5..6a4922e34a6dcc497fdb5f672d94318fe0afe601 100644
--- a/src/mol-gl/compute/grid3d.ts
+++ b/src/mol-gl/compute/grid3d.ts
@@ -225,8 +225,8 @@ export function createGrid3dComputeRenderable<S extends RenderableSchema, P, CS>
 
 function resetGl(webgl: WebGLContext, w: number) {
     const { gl, state } = webgl;
-    gl.viewport(0, 0, w, w);
-    gl.scissor(0, 0, w, w);
+    state.viewport(0, 0, w, w);
+    state.scissor(0, 0, w, w);
     state.disable(gl.SCISSOR_TEST);
     state.disable(gl.BLEND);
     state.disable(gl.DEPTH_TEST);
diff --git a/src/mol-gl/compute/histogram-pyramid/reduction.ts b/src/mol-gl/compute/histogram-pyramid/reduction.ts
index 8d162f5e99e8e69cf1f5fcb188a956336682d59f..43aaddf292b62a701c48f4cdb273beda069cc255 100644
--- a/src/mol-gl/compute/histogram-pyramid/reduction.ts
+++ b/src/mol-gl/compute/histogram-pyramid/reduction.ts
@@ -122,7 +122,7 @@ export interface HistogramPyramid {
 
 export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture, scale: Vec2, gridTexDim: Vec3): HistogramPyramid {
     if (isTimingMode) ctx.timer.mark('createHistogramPyramid');
-    const { gl } = ctx;
+    const { gl, state } = ctx;
     const w = inputTexture.getWidth();
     const h = inputTexture.getHeight();
 
@@ -146,7 +146,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     const framebuffer = getFramebuffer('pyramid', ctx);
     pyramidTex.attachFramebuffer(framebuffer, 0);
 
-    gl.viewport(0, 0, maxSizeX, maxSizeY);
+    state.viewport(0, 0, maxSizeX, maxSizeY);
     if (isWebGL2(gl)) {
         gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
     } else {
@@ -157,7 +157,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     for (let i = 0; i < levels; ++i) levelTexturesFramebuffers.push(getLevelTextureFramebuffer(ctx, i));
 
     const renderable = getHistopyramidReductionRenderable(ctx, inputTexture, levelTexturesFramebuffers[0].texture);
-    ctx.state.currentRenderItemId = -1;
+    state.currentRenderItemId = -1;
     setRenderingDefaults(ctx);
 
     let offset = 0;
@@ -176,15 +176,15 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
             ValueCell.update(renderable.values.tPreviousLevel, levelTexturesFramebuffers[levels - i].texture);
             renderable.update();
         }
-        ctx.state.currentRenderItemId = -1;
-        gl.viewport(0, 0, size, size);
-        gl.scissor(0, 0, size, size);
+        state.currentRenderItemId = -1;
+        state.viewport(0, 0, size, size);
+        state.scissor(0, 0, size, size);
         if (isWebGL2(gl)) {
             gl.clearBufferiv(gl.COLOR, 0, [0, 0, 0, 0]);
         } else {
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
-        gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+        state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
         renderable.render();
 
         pyramidTex.bind(0);
@@ -197,7 +197,7 @@ export function createHistogramPyramid(ctx: WebGLContext, inputTexture: Texture,
     gl.finish();
     if (isTimingMode) ctx.timer.markEnd('createHistogramPyramid');
 
-    // printTexture(ctx, pyramidTex, 2)
+    // printTextureImage(readTexture(ctx, pyramidTex), { scale: 0.75 });
 
     //
 
diff --git a/src/mol-gl/compute/histogram-pyramid/sum.ts b/src/mol-gl/compute/histogram-pyramid/sum.ts
index a1cd5919a7bf5632b87d6ca0c8ea274ecff1caf4..65c36515d80ac9d2f1f7e3e87118c90fe240bcaf 100644
--- a/src/mol-gl/compute/histogram-pyramid/sum.ts
+++ b/src/mol-gl/compute/histogram-pyramid/sum.ts
@@ -68,7 +68,7 @@ const sumInts = new Int32Array(4);
 
 export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture) {
     if (isTimingMode) ctx.timer.mark('getHistopyramidSum');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
 
     const renderable = getHistopyramidSumRenderable(ctx, pyramidTopTexture);
     ctx.state.currentRenderItemId = -1;
@@ -89,7 +89,7 @@ export function getHistopyramidSum(ctx: WebGLContext, pyramidTopTexture: Texture
 
     setRenderingDefaults(ctx);
 
-    gl.viewport(0, 0, 1, 1);
+    state.viewport(0, 0, 1, 1);
     renderable.render();
     gl.finish();
 
diff --git a/src/mol-gl/compute/marching-cubes/active-voxels.ts b/src/mol-gl/compute/marching-cubes/active-voxels.ts
index b16014c011b8eef48a74c800b69c595b463bdee7..e4fe0173a728020bf95d0a2642a7c36ce9ba35b3 100644
--- a/src/mol-gl/compute/marching-cubes/active-voxels.ts
+++ b/src/mol-gl/compute/marching-cubes/active-voxels.ts
@@ -85,7 +85,7 @@ function setRenderingDefaults(ctx: WebGLContext) {
 
 export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, isoValue: number, gridScale: Vec2) {
     if (isTimingMode) ctx.timer.mark('calcActiveVoxels');
-    const { gl, resources } = ctx;
+    const { gl, state, resources } = ctx;
     const width = volumeData.getWidth();
     const height = volumeData.getHeight();
 
@@ -106,15 +106,16 @@ export function calcActiveVoxels(ctx: WebGLContext, volumeData: Texture, gridDim
 
     activeVoxelsTex.attachFramebuffer(framebuffer, 0);
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
-    gl.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
+    state.scissor(0, 0, gridTexDim[0], gridTexDim[1]);
     renderable.render();
 
     // console.log('gridScale', gridScale, 'gridTexDim', gridTexDim, 'gridDim', gridDim);
     // console.log('volumeData', volumeData);
     // console.log('at', readTexture(ctx, activeVoxelsTex));
+    // printTextureImage(readTexture(ctx, activeVoxelsTex), { scale: 0.75 });
 
     gl.finish();
     if (isTimingMode) ctx.timer.markEnd('calcActiveVoxels');
diff --git a/src/mol-gl/compute/marching-cubes/isosurface.ts b/src/mol-gl/compute/marching-cubes/isosurface.ts
index 93263e22713b409f2e343f85bd84d648e5e20085..62af9563c3315e72f2ed814250a2d4cb007ff6a7 100644
--- a/src/mol-gl/compute/marching-cubes/isosurface.ts
+++ b/src/mol-gl/compute/marching-cubes/isosurface.ts
@@ -42,12 +42,13 @@ const IsosurfaceSchema = {
 
     dPackedGroup: DefineSpec('boolean'),
     dAxisOrder: DefineSpec('string', ['012', '021', '102', '120', '201', '210']),
+    dConstantGroup: DefineSpec('boolean'),
 };
 type IsosurfaceValues = Values<typeof IsosurfaceSchema>
 
 const IsosurfaceName = 'isosurface';
 
-function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3): ComputeRenderable<IsosurfaceValues> {
+function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean): ComputeRenderable<IsosurfaceValues> {
     if (ctx.namedComputeRenderables[IsosurfaceName]) {
         const v = ctx.namedComputeRenderables[IsosurfaceName].values as IsosurfaceValues;
 
@@ -66,17 +67,18 @@ function getIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture
         ValueCell.update(v.uGridTransform, transform);
         ValueCell.update(v.uScale, scale);
 
-        ValueCell.update(v.dPackedGroup, packedGroup);
+        ValueCell.updateIfChanged(v.dPackedGroup, packedGroup);
         ValueCell.updateIfChanged(v.dAxisOrder, axisOrder.join(''));
+        ValueCell.updateIfChanged(v.dConstantGroup, constantGroup);
 
         ctx.namedComputeRenderables[IsosurfaceName].update();
     } else {
-        ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder);
+        ctx.namedComputeRenderables[IsosurfaceName] = createIsosurfaceRenderable(ctx, activeVoxelsPyramid, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
     }
     return ctx.namedComputeRenderables[IsosurfaceName];
 }
 
-function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3) {
+function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Texture, activeVoxelsBase: Texture, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, levels: number, scale: Vec2, count: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean) {
     // console.log('uSize', Math.pow(2, levels))
     const values: IsosurfaceValues = {
         ...QuadValues,
@@ -99,6 +101,7 @@ function createIsosurfaceRenderable(ctx: WebGLContext, activeVoxelsPyramid: Text
 
         dPackedGroup: ValueCell.create(packedGroup),
         dAxisOrder: ValueCell.create(axisOrder.join('')),
+        dConstantGroup: ValueCell.create(constantGroup),
     };
 
     const schema = { ...IsosurfaceSchema };
@@ -119,12 +122,12 @@ function setRenderingDefaults(ctx: WebGLContext) {
     state.clearColor(0, 0, 0, 0);
 }
 
-export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
+export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Texture, volumeData: Texture, histogramPyramid: HistogramPyramid, gridDim: Vec3, gridTexDim: Vec3, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
     const { drawBuffers } = ctx.extensions;
     if (!drawBuffers) throw new Error('need WebGL draw buffers');
 
     if (isTimingMode) ctx.timer.mark('createIsosurfaceBuffers');
-    const { gl, resources, extensions } = ctx;
+    const { gl, state, resources, extensions } = ctx;
     const { pyramidTex, height, levels, scale, count } = histogramPyramid;
     const width = pyramidTex.getWidth();
 
@@ -178,7 +181,7 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     groupTexture.attachFramebuffer(framebuffer, 1);
     normalTexture.attachFramebuffer(framebuffer, 2);
 
-    const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder);
+    const renderable = getIsosurfaceRenderable(ctx, pyramidTex, activeVoxelsBase, volumeData, gridDim, gridTexDim, transform, isoValue, levels, scale, count, invert, packedGroup, axisOrder, constantGroup);
     ctx.state.currentRenderItemId = -1;
 
     framebuffer.bind();
@@ -189,13 +192,17 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
     ]);
 
     setRenderingDefaults(ctx);
-    gl.viewport(0, 0, width, height);
+    state.viewport(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     renderable.render();
 
     gl.finish();
     if (isTimingMode) ctx.timer.markEnd('createIsosurfaceBuffers');
 
+    // printTextureImage(readTexture(ctx, vertexTexture, new Float32Array(width * height * 4)), { scale: 0.75 });
+    // printTextureImage(readTexture(ctx, groupTexture, new Uint8Array(width * height * 4)), { scale: 0.75 });
+    // printTextureImage(readTexture(ctx, normalTexture, new Float32Array(width * height * 4)), { scale: 0.75 });
+
     return { vertexTexture, groupTexture, normalTexture, vertexCount: count };
 }
 
@@ -210,11 +217,11 @@ export function createIsosurfaceBuffers(ctx: WebGLContext, activeVoxelsBase: Tex
  *
  * Implementation based on http://www.miaumiau.cat/2016/10/stream-compaction-in-webgl/
  */
-export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
+export function extractIsosurface(ctx: WebGLContext, volumeData: Texture, gridDim: Vec3, gridTexDim: Vec3, gridTexScale: Vec2, transform: Mat4, isoValue: number, invert: boolean, packedGroup: boolean, axisOrder: Vec3, constantGroup: boolean, vertexTexture?: Texture, groupTexture?: Texture, normalTexture?: Texture) {
     if (isTimingMode) ctx.timer.mark('extractIsosurface');
     const activeVoxelsTex = calcActiveVoxels(ctx, volumeData, gridDim, gridTexDim, isoValue, gridTexScale);
     const compacted = createHistogramPyramid(ctx, activeVoxelsTex, gridTexScale, gridTexDim);
-    const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, transform, isoValue, invert, packedGroup, axisOrder, vertexTexture, groupTexture, normalTexture);
+    const gv = createIsosurfaceBuffers(ctx, activeVoxelsTex, volumeData, compacted, gridDim, gridTexDim, transform, isoValue, invert, packedGroup, axisOrder, constantGroup, vertexTexture, groupTexture, normalTexture);
     if (isTimingMode) ctx.timer.markEnd('extractIsosurface');
 
     return gv;
diff --git a/src/mol-gl/compute/util.ts b/src/mol-gl/compute/util.ts
index 2e759efbb3def192fe627db0c448a0b81ba39bc5..044826ae0210215a3cdb0672ce5bfb5b134c5888 100644
--- a/src/mol-gl/compute/util.ts
+++ b/src/mol-gl/compute/util.ts
@@ -75,9 +75,9 @@ export function getSharedCopyRenderable(ctx: WebGLContext, texture: Texture) {
 const ReadTextureName = 'read-texture';
 const ReadAlphaTextureName = 'read-alpha-texture';
 
-export function readTexture(ctx: WebGLContext, texture: Texture) {
+export function readTexture<T extends Uint8Array | Float32Array | Int32Array = Uint8Array>(ctx: WebGLContext, texture: Texture, array?: T) {
     const { gl, resources } = ctx;
-    if (texture.type !== gl.UNSIGNED_BYTE) throw new Error('unsupported texture type');
+    if (!array && texture.type !== gl.UNSIGNED_BYTE) throw new Error('unsupported texture type');
 
     if (!ctx.namedFramebuffers[ReadTextureName]) {
         ctx.namedFramebuffers[ReadTextureName] = resources.framebuffer();
@@ -86,7 +86,7 @@ export function readTexture(ctx: WebGLContext, texture: Texture) {
 
     const width = texture.getWidth();
     const height = texture.getHeight();
-    const array = new Uint8Array(width * height * 4);
+    if (!array) array = new Uint8Array(width * height * 4) as T;
     framebuffer.bind();
     texture.attachFramebuffer(framebuffer, 0);
     ctx.readPixels(0, 0, width, height, array);
@@ -125,8 +125,8 @@ export function readAlphaTexture(ctx: WebGLContext, texture: Texture) {
     state.clearColor(0, 0, 0, 0);
     state.blendFunc(gl.ONE, gl.ONE);
     state.blendEquation(gl.FUNC_ADD);
-    gl.viewport(0, 0, width, height);
-    gl.scissor(0, 0, width, height);
+    state.viewport(0, 0, width, height);
+    state.scissor(0, 0, width, height);
     gl.clear(gl.COLOR_BUFFER_BIT);
     copy.render();
 
diff --git a/src/mol-gl/renderable.ts b/src/mol-gl/renderable.ts
index e0c4db03086fba4c7e3750fc225569651b02b397..c8758d53cdac056fab72ce22a698b1ee70f6cd37 100644
--- a/src/mol-gl/renderable.ts
+++ b/src/mol-gl/renderable.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,7 +10,6 @@ import { GraphicsRenderItem, ComputeRenderItem, GraphicsRenderVariant } from './
 import { ValueCell } from '../mol-util';
 import { idFactory } from '../mol-util/id-factory';
 import { clamp } from '../mol-math/interpolate';
-import { Textures } from './webgl/texture';
 
 const getNextRenderableId = idFactory();
 
@@ -30,7 +29,7 @@ export interface Renderable<T extends RenderableValues> {
     readonly values: T
     readonly state: RenderableState
 
-    render: (variant: GraphicsRenderVariant, sharedTexturesList?: Textures) => void
+    render: (variant: GraphicsRenderVariant, sharedTexturesCount: number) => void
     getProgram: (variant: GraphicsRenderVariant) => Program
     update: () => void
     dispose: () => void
@@ -43,11 +42,11 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
         values,
         state,
 
-        render: (variant: GraphicsRenderVariant, sharedTexturesList?: Textures) => {
+        render: (variant: GraphicsRenderVariant, sharedTexturesCount: number) => {
             if (values.uAlpha && values.alpha) {
                 ValueCell.updateIfChanged(values.uAlpha, clamp(values.alpha.ref.value * state.alphaFactor, 0, 1));
             }
-            renderItem.render(variant, sharedTexturesList);
+            renderItem.render(variant, sharedTexturesCount);
         },
         getProgram: (variant: GraphicsRenderVariant) => renderItem.getProgram(variant),
         update: () => renderItem.update(),
@@ -73,7 +72,7 @@ export function createComputeRenderable<T extends Values<RenderableSchema>>(rend
         id: getNextRenderableId(),
         values,
 
-        render: () => renderItem.render('compute'),
+        render: () => renderItem.render('compute', 0),
         update: () => renderItem.update(),
         dispose: () => renderItem.destroy()
     };
diff --git a/src/mol-gl/renderable/cylinders.ts b/src/mol-gl/renderable/cylinders.ts
index 5366aaae108f4f98d631edb538c53b9b8151cd45..db145d3edcf2eeff50f53b50d2eb07134fd8e5c4 100644
--- a/src/mol-gl/renderable/cylinders.ts
+++ b/src/mol-gl/renderable/cylinders.ts
@@ -23,12 +23,12 @@ export const CylindersSchema = {
     elements: ElementsSpec('uint32'),
 
     padding: ValueSpec('number'),
-    uDoubleSided: UniformSpec('b'),
+    uDoubleSided: UniformSpec('b', 'material'),
     dIgnoreLight: DefineSpec('boolean'),
     dXrayShaded: DefineSpec('boolean'),
     dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
-    uBumpFrequency: UniformSpec('f'),
-    uBumpAmplitude: UniformSpec('f'),
+    uBumpFrequency: UniformSpec('f', 'material'),
+    uBumpAmplitude: UniformSpec('f', 'material'),
 };
 export type CylindersSchema = typeof CylindersSchema
 export type CylindersValues = Values<CylindersSchema>
diff --git a/src/mol-gl/renderable/direct-volume.ts b/src/mol-gl/renderable/direct-volume.ts
index 533235e9cdcb45b3bf36d94f95916785c6ffdd21..2b449fb262ed3a865e18634dc6b58ad338f28c72 100644
--- a/src/mol-gl/renderable/direct-volume.ts
+++ b/src/mol-gl/renderable/direct-volume.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -26,7 +26,7 @@ export const DirectVolumeSchema = {
     uTransform: UniformSpec('m4'),
     uGridDim: UniformSpec('v3'),
     tTransferTex: TextureSpec('image-uint8', 'alpha', 'ubyte', 'linear'),
-    uTransferScale: UniformSpec('f'),
+    uTransferScale: UniformSpec('f', 'material'),
 
     dGridTexType: DefineSpec('string', ['2d', '3d']),
     uGridTexDim: UniformSpec('v3'),
diff --git a/src/mol-gl/renderable/lines.ts b/src/mol-gl/renderable/lines.ts
index a385e1922c585c3690e5612ed421129319209716..021ebba99d6fffad2d420da91de73128970c9ea1 100644
--- a/src/mol-gl/renderable/lines.ts
+++ b/src/mol-gl/renderable/lines.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -20,7 +20,7 @@ export const LinesSchema = {
     aEnd: AttributeSpec('float32', 3, 0),
     elements: ElementsSpec('uint32'),
     dLineSizeAttenuation: DefineSpec('boolean'),
-    uDoubleSided: UniformSpec('b'),
+    uDoubleSided: UniformSpec('b', 'material'),
     dFlipSided: DefineSpec('boolean'),
 };
 export type LinesSchema = typeof LinesSchema
diff --git a/src/mol-gl/renderable/mesh.ts b/src/mol-gl/renderable/mesh.ts
index c3da8f71d52720e5123d31f40419a0506946e241..fa5244d23e7e8aa87a87febf33619dee44861a5c 100644
--- a/src/mol-gl/renderable/mesh.ts
+++ b/src/mol-gl/renderable/mesh.ts
@@ -17,14 +17,15 @@ export const MeshSchema = {
     aPosition: AttributeSpec('float32', 3, 0),
     aNormal: AttributeSpec('float32', 3, 0),
     elements: ElementsSpec('uint32'),
+    dVaryingGroup: DefineSpec('boolean'),
     dFlatShaded: DefineSpec('boolean'),
-    uDoubleSided: UniformSpec('b'),
+    uDoubleSided: UniformSpec('b', 'material'),
     dFlipSided: DefineSpec('boolean'),
     dIgnoreLight: DefineSpec('boolean'),
     dXrayShaded: DefineSpec('boolean'),
     dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
-    uBumpFrequency: UniformSpec('f'),
-    uBumpAmplitude: UniformSpec('f'),
+    uBumpFrequency: UniformSpec('f', 'material'),
+    uBumpAmplitude: UniformSpec('f', 'material'),
     meta: ValueSpec('unknown')
 } as const;
 export type MeshSchema = typeof MeshSchema
diff --git a/src/mol-gl/renderable/schema.ts b/src/mol-gl/renderable/schema.ts
index 6880287cbf44de7927555000c09b8a04b38487cf..89b0752120f434019d180a3fab7f4773ab31dd77 100644
--- a/src/mol-gl/renderable/schema.ts
+++ b/src/mol-gl/renderable/schema.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { ValueCell } from '../../mol-util';
@@ -36,6 +37,7 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
     const attributeValues: AttributeValues = {};
     const defineValues: DefineValues = {};
     const textureValues: TextureValues = {};
+    const materialTextureValues: TextureValues = {};
     const uniformValues: UniformValues = {};
     const materialUniformValues: UniformValues = {};
     const bufferedUniformValues: UniformValues = {};
@@ -44,7 +46,10 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
         if (spec.type === 'attribute') attributeValues[k] = values[k];
         if (spec.type === 'define') defineValues[k] = values[k];
         // check if k exists in values to exclude global textures
-        if (spec.type === 'texture' && values[k] !== undefined) textureValues[k] = values[k];
+        if (spec.type === 'texture' && values[k] !== undefined) {
+            if (spec.variant === 'material') materialTextureValues[k] = values[k];
+            else textureValues[k] = values[k];
+        }
         // check if k exists in values to exclude global uniforms
         if (spec.type === 'uniform' && values[k] !== undefined) {
             if (spec.variant === 'material') materialUniformValues[k] = values[k];
@@ -52,7 +57,7 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
             else uniformValues[k] = values[k];
         }
     });
-    return { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues, bufferedUniformValues };
+    return { attributeValues, defineValues, textureValues, materialTextureValues, uniformValues, materialUniformValues, bufferedUniformValues };
 }
 
 export type Versions<T extends RenderableValues> = { [k in keyof T]: number }
@@ -76,9 +81,9 @@ export function UniformSpec<K extends UniformKind>(kind: K, variant?: 'material'
     return { type: 'uniform', kind, variant };
 }
 
-export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter }
-export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter): TextureSpec<K> {
-    return { type: 'texture', kind, format, dataType, filter };
+export type TextureSpec<K extends TextureKind> = { type: 'texture', kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material' }
+export function TextureSpec<K extends TextureKind>(kind: K, format: TextureFormat, dataType: TextureType, filter: TextureFilter, variant?: 'material'): TextureSpec<K> {
+    return { type: 'texture', kind, format, dataType, filter, variant };
 }
 
 export type ElementsSpec<K extends ElementsKind> = { type: 'elements', kind: K }
@@ -163,6 +168,11 @@ export type GlobalUniformValues = Values<GlobalUniformSchema>
 
 export const GlobalTextureSchema = {
     tDepth: TextureSpec('texture', 'depth', 'ushort', 'nearest'),
+
+    // dpoit
+    tDpoitDepth: TextureSpec('texture', 'rg', 'float', 'nearest'),
+    tDpoitFrontColor: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tDpoitBackColor: TextureSpec('texture', 'rgba', 'float', 'nearest')
 } as const;
 export type GlobalTextureSchema = typeof GlobalTextureSchema
 export type GlobalTextureValues = Values<GlobalTextureSchema>
@@ -194,7 +204,7 @@ export const SizeSchema = {
     uSizeTexDim: UniformSpec('v2'),
     tSize: TextureSpec('image-uint8', 'rgb', 'ubyte', 'nearest'),
     dSizeType: DefineSpec('string', ['uniform', 'attribute', 'instance', 'group', 'groupInstance']),
-    uSizeFactor: UniformSpec('f'),
+    uSizeFactor: UniformSpec('f', 'material'),
 } as const;
 export type SizeSchema = typeof SizeSchema
 export type SizeValues = Values<SizeSchema>
@@ -232,7 +242,7 @@ export const TransparencySchema = {
     uTransparencyGridDim: UniformSpec('v3'),
     uTransparencyGridTransform: UniformSpec('v4'),
     tTransparencyGrid: TextureSpec('texture', 'alpha', 'ubyte', 'linear'),
-    dTransparencyType: DefineSpec('string', ['instance', 'groupInstance', 'volumeInstance']),
+    dTransparencyType: DefineSpec('string', ['instance', 'groupInstance', 'volumeInstance'])
 } as const;
 export type TransparencySchema = typeof TransparencySchema
 export type TransparencyValues = Values<TransparencySchema>
@@ -270,14 +280,15 @@ export const BaseSchema = {
     ...ClippingSchema,
 
     dLightCount: DefineSpec('number'),
+    dColorMarker: DefineSpec('boolean'),
 
     dClipObjectCount: DefineSpec('number'),
     dClipVariant: DefineSpec('string', ['instance', 'pixel']),
-    uClipObjectType: UniformSpec('i[]'),
-    uClipObjectInvert: UniformSpec('b[]'),
-    uClipObjectPosition: UniformSpec('v3[]'),
-    uClipObjectRotation: UniformSpec('v4[]'),
-    uClipObjectScale: UniformSpec('v3[]'),
+    uClipObjectType: UniformSpec('i[]', 'material'),
+    uClipObjectInvert: UniformSpec('b[]', 'material'),
+    uClipObjectPosition: UniformSpec('v3[]', 'material'),
+    uClipObjectRotation: UniformSpec('v4[]', 'material'),
+    uClipObjectScale: UniformSpec('v3[]', 'material'),
 
     aInstance: AttributeSpec('float32', 1, 1),
     /**
@@ -322,4 +333,4 @@ export const BaseSchema = {
     invariantBoundingSphere: ValueSpec('sphere'),
 } as const;
 export type BaseSchema = typeof BaseSchema
-export type BaseValues = Values<BaseSchema>
\ No newline at end of file
+export type BaseValues = Values<BaseSchema>
diff --git a/src/mol-gl/renderable/spheres.ts b/src/mol-gl/renderable/spheres.ts
index 60e316d81fefdb9d947c5dd961c451708b616a14..73c0e750754cb7617e34d23d32b2c9c6bf261012 100644
--- a/src/mol-gl/renderable/spheres.ts
+++ b/src/mol-gl/renderable/spheres.ts
@@ -20,12 +20,12 @@ export const SpheresSchema = {
     elements: ElementsSpec('uint32'),
 
     padding: ValueSpec('number'),
-    uDoubleSided: UniformSpec('b'),
+    uDoubleSided: UniformSpec('b', 'material'),
     dIgnoreLight: DefineSpec('boolean'),
     dXrayShaded: DefineSpec('boolean'),
     dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
-    uBumpFrequency: UniformSpec('f'),
-    uBumpAmplitude: UniformSpec('f'),
+    uBumpFrequency: UniformSpec('f', 'material'),
+    uBumpAmplitude: UniformSpec('f', 'material'),
 };
 export type SpheresSchema = typeof SpheresSchema
 export type SpheresValues = Values<SpheresSchema>
diff --git a/src/mol-gl/renderable/text.ts b/src/mol-gl/renderable/text.ts
index 8f6e145e659b8197c8e2ae17069546afb4b0e6c9..364d07c5b07017aa419eefb668e8ae4601942686 100644
--- a/src/mol-gl/renderable/text.ts
+++ b/src/mol-gl/renderable/text.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -24,13 +24,13 @@ export const TextSchema = {
     tFont: TextureSpec('image-uint8', 'alpha', 'ubyte', 'linear'),
     padding: ValueSpec('number'),
 
-    uBorderWidth: UniformSpec('f'),
-    uBorderColor: UniformSpec('v3'),
-    uOffsetX: UniformSpec('f'),
-    uOffsetY: UniformSpec('f'),
-    uOffsetZ: UniformSpec('f'),
-    uBackgroundColor: UniformSpec('v3'),
-    uBackgroundOpacity: UniformSpec('f'),
+    uBorderWidth: UniformSpec('f', 'material'),
+    uBorderColor: UniformSpec('v3', 'material'),
+    uOffsetX: UniformSpec('f', 'material'),
+    uOffsetY: UniformSpec('f', 'material'),
+    uOffsetZ: UniformSpec('f', 'material'),
+    uBackgroundColor: UniformSpec('v3', 'material'),
+    uBackgroundOpacity: UniformSpec('f', 'material'),
 };
 export type TextSchema = typeof TextSchema
 export type TextValues = Values<TextSchema>
diff --git a/src/mol-gl/renderable/texture-mesh.ts b/src/mol-gl/renderable/texture-mesh.ts
index e997a9bb7c5c0e390d77f6e80d77d730c376533a..2436be94e4d17229f4ae060007d6c4b3b80c1d2d 100644
--- a/src/mol-gl/renderable/texture-mesh.ts
+++ b/src/mol-gl/renderable/texture-mesh.ts
@@ -17,15 +17,15 @@ export const TextureMeshSchema = {
     tPosition: TextureSpec('texture', 'rgb', 'float', 'nearest'),
     tGroup: TextureSpec('texture', 'alpha', 'float', 'nearest'),
     tNormal: TextureSpec('texture', 'rgb', 'float', 'nearest'),
-
+    dVaryingGroup: DefineSpec('boolean'),
     dFlatShaded: DefineSpec('boolean'),
-    uDoubleSided: UniformSpec('b'),
+    uDoubleSided: UniformSpec('b', 'material'),
     dFlipSided: DefineSpec('boolean'),
     dIgnoreLight: DefineSpec('boolean'),
     dXrayShaded: DefineSpec('boolean'),
     dTransparentBackfaces: DefineSpec('string', ['off', 'on', 'opaque']),
-    uBumpFrequency: UniformSpec('f'),
-    uBumpAmplitude: UniformSpec('f'),
+    uBumpFrequency: UniformSpec('f', 'material'),
+    uBumpAmplitude: UniformSpec('f', 'material'),
     meta: ValueSpec('unknown')
 };
 export type TextureMeshSchema = typeof TextureMeshSchema
diff --git a/src/mol-gl/renderable/util.ts b/src/mol-gl/renderable/util.ts
index 87601da6a1dd366dada2eb728ad87ef75a3fb3aa..cb54c9a737dd471d6b3cf3e6b3571111618abbf6 100644
--- a/src/mol-gl/renderable/util.ts
+++ b/src/mol-gl/renderable/util.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -8,6 +8,7 @@ import { Sphere3D } from '../../mol-math/geometry';
 import { Vec3, Mat4 } from '../../mol-math/linear-algebra';
 import { BoundaryHelper } from '../../mol-math/geometry/boundary-helper';
 import { TextureFilter } from '../webgl/texture';
+import { arrayMinMax } from '../../mol-util/array';
 
 export function calculateTextureInfo(n: number, itemSize: number) {
     n = Math.max(n, 2); // observed issues with 1 pixel textures
@@ -42,7 +43,8 @@ export function createTextureImage<T extends Uint8Array | Float32Array>(n: numbe
 const DefaultPrintImageOptions = {
     scale: 1,
     pixelated: false,
-    id: 'molstar.debug.image'
+    id: 'molstar.debug.image',
+    normalize: false,
 };
 export type PrintImageOptions = typeof DefaultPrintImageOptions
 
@@ -58,7 +60,17 @@ export function printTextureImage(textureImage: TextureImage<any>, options: Part
             }
         }
     } else if (itemSize === 4) {
-        data.set(array);
+        if (options.normalize) {
+            const [min, max] = arrayMinMax(array);
+            for (let i = 0, il = width * height * 4; i < il; i += 4) {
+                data[i] = ((array[i] - min) / (max - min)) * 255;
+                data[i + 1] = ((array[i + 1] - min) / (max - min)) * 255;
+                data[i + 2] = ((array[i + 2] - min) / (max - min)) * 255;
+                data[i + 3] = 255;
+            }
+        } else {
+            data.set(array);
+        }
     } else {
         console.warn(`itemSize '${itemSize}' not supported`);
     }
diff --git a/src/mol-gl/renderer.ts b/src/mol-gl/renderer.ts
index 93d7e73fd85dfb257d129112ba83102e93557052..086f6db4086bed82767ccb56c8d8bb7bec60f58a 100644
--- a/src/mol-gl/renderer.ts
+++ b/src/mol-gl/renderer.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { Viewport } from '../mol-canvas3d/camera/util';
@@ -64,12 +65,15 @@ interface Renderer {
     renderDepthTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingDepth: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderMarkingMask: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
-    renderBlended: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderBlended: (group: Scene, camera: ICamera) => void
     renderBlendedOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderBlendedVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderWboitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
     renderWboitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderDpoitOpaque: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
+    renderDpoitTransparent: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => void
+    renderDpoitVolume: (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => void
 
     setProps: (props: Partial<RendererProps>) => void
     setViewport: (x: number, y: number, width: number, height: number) => void
@@ -89,6 +93,7 @@ export const RendererParams = {
     interiorColorFlag: PD.Boolean(true, { label: 'Use Interior Color' }),
     interiorColor: PD.Color(Color.fromNormalizedRgb(0.3, 0.3, 0.3)),
 
+    colorMarker: PD.Boolean(true, { description: 'Enable color marker' }),
     highlightColor: PD.Color(Color.fromNormalizedRgb(1.0, 0.4, 0.6)),
     selectColor: PD.Color(Color.fromNormalizedRgb(0.2, 1.0, 0.1)),
     highlightStrength: PD.Numeric(0.3, { min: 0.0, max: 1.0, step: 0.1 }),
@@ -140,7 +145,7 @@ namespace Renderer {
     const enum Flag {
         None = 0,
         BlendedFront = 1,
-        BlendedBack = 2
+        BlendedBack = 2,
     }
 
     const enum Mask {
@@ -246,6 +251,10 @@ namespace Renderer {
                 ValueCell.update(r.values.dLightCount, light.count);
                 definesNeedUpdate = true;
             }
+            if (r.values.dColorMarker.ref.value !== p.colorMarker) {
+                ValueCell.update(r.values.dColorMarker, p.colorMarker);
+                definesNeedUpdate = true;
+            }
             if (definesNeedUpdate) r.update();
 
             const program = r.getProgram(variant);
@@ -258,11 +267,12 @@ namespace Renderer {
             if (globalUniformsNeedUpdate) {
                 // console.log('globalUniformsNeedUpdate')
                 program.setUniforms(globalUniformList);
+                program.bindTextures(sharedTexturesList, 0);
                 globalUniformsNeedUpdate = false;
             }
 
             if (r.values.dGeometryType.ref.value === 'directVolume') {
-                if (variant !== 'colorWboit' && variant !== 'colorBlended') {
+                if (variant !== 'colorDpoit' && variant !== 'colorWboit' && variant !== 'colorBlended') {
                     return; // only color supported
                 }
 
@@ -315,7 +325,7 @@ namespace Renderer {
                 }
             }
 
-            r.render(variant, sharedTexturesList);
+            r.render(variant, sharedTexturesList.length);
         };
 
         const update = (camera: ICamera) => {
@@ -353,8 +363,8 @@ namespace Renderer {
             state.colorMask(true, true, true, true);
 
             const { x, y, width, height } = viewport;
-            gl.viewport(x, y, width, height);
-            gl.scissor(x, y, width, height);
+            state.viewport(x, y, width, height);
+            state.scissor(x, y, width, height);
 
             globalUniformsNeedUpdate = true;
             state.currentRenderItemId = -1;
@@ -469,9 +479,13 @@ namespace Renderer {
             if (isTimingMode) ctx.timer.markEnd('Renderer.renderMarkingMask');
         };
 
-        const renderBlended = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
-            renderBlendedOpaque(group, camera, depthTexture);
-            renderBlendedTransparent(group, camera, depthTexture);
+        const renderBlended = (scene: Scene, camera: ICamera) => {
+            if (scene.hasOpaque) {
+                renderBlendedOpaque(scene, camera, null);
+            }
+            if (scene.opacityAverage < 1) {
+                renderBlendedTransparent(scene, camera, null);
+            }
         };
 
         const renderBlendedOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
@@ -585,13 +599,78 @@ namespace Renderer {
                 // TODO: simplify, handle in renderable.state???
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
-                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dGeometryType.ref.value === 'directVolume' || r.values.dPointStyle?.ref.value === 'fuzzy' || r.values.dGeometryType.ref.value === 'text' || r.values.dXrayShaded?.ref.value) {
                     renderObject(r, 'colorWboit', Flag.None);
                 }
             }
             if (isTimingMode) ctx.timer.markEnd('Renderer.renderWboitTransparent');
         };
 
+        const renderDpoitOpaque = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitOpaque');
+            state.disable(gl.BLEND);
+            state.enable(gl.DEPTH_TEST);
+            state.depthMask(true);
+
+            updateInternal(group, camera, depthTexture, Mask.Opaque, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                // TODO: simplify, handle in renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if ((alpha === 1 && r.values.transparencyAverage.ref.value !== 1 && r.values.dPointStyle?.ref.value !== 'fuzzy' && !r.values.dXrayShaded?.ref.value) || r.values.dTransparentBackfaces?.ref.value === 'opaque') {
+                    renderObject(r, 'colorDpoit', Flag.None);
+                }
+            }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitOpaque');
+        };
+
+        const renderDpoitTransparent = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null, dpoitTextures: { depth: Texture, frontColor: Texture, backColor: Texture }) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitTransparent');
+
+            state.enable(gl.BLEND);
+
+            arrayMapUpsert(sharedTexturesList, 'tDpoitDepth', dpoitTextures.depth);
+            arrayMapUpsert(sharedTexturesList, 'tDpoitFrontColor', dpoitTextures.frontColor);
+            arrayMapUpsert(sharedTexturesList, 'tDpoitBackColor', dpoitTextures.backColor);
+
+            updateInternal(group, camera, depthTexture, Mask.Transparent, false);
+
+            const { renderables } = group;
+
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+
+                // TODO: simplify, handle in renderable.state???
+                // uAlpha is updated in "render" so we need to recompute it here
+                const alpha = clamp(r.values.alpha.ref.value * r.state.alphaFactor, 0, 1);
+                if (alpha < 1 || r.values.transparencyAverage.ref.value > 0 || r.values.dPointStyle?.ref.value === 'fuzzy' || !!r.values.uBackgroundColor || r.values.dXrayShaded?.ref.value) {
+                    renderObject(r, 'colorDpoit', Flag.None);
+                }
+            }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitTransparent');
+        };
+
+        const renderDpoitVolume = (group: Scene.Group, camera: ICamera, depthTexture: Texture | null) => {
+            if (isTimingMode) ctx.timer.mark('Renderer.renderDpoitVolume');
+            state.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);
+            state.enable(gl.BLEND);
+
+            updateInternal(group, camera, depthTexture, Mask.Transparent, false);
+
+            const { renderables } = group;
+            for (let i = 0, il = renderables.length; i < il; ++i) {
+                const r = renderables[i];
+                if (r.values.dGeometryType.ref.value === 'directVolume') {
+                    renderObject(r, 'colorDpoit', Flag.None);
+                }
+            }
+            if (isTimingMode) ctx.timer.markEnd('Renderer.renderDpoitVolume');
+        };
+
         return {
             clear: (toBackgroundColor: boolean, ignoreTransparentBackground?: boolean) => {
                 state.enable(gl.SCISSOR_TEST);
@@ -635,6 +714,9 @@ namespace Renderer {
             renderBlendedVolume,
             renderWboitOpaque,
             renderWboitTransparent,
+            renderDpoitOpaque,
+            renderDpoitTransparent,
+            renderDpoitVolume,
 
             setProps: (props: Partial<RendererProps>) => {
                 if (props.backgroundColor !== undefined && props.backgroundColor !== p.backgroundColor) {
@@ -661,6 +743,9 @@ namespace Renderer {
                     ValueCell.update(globalUniforms.uInteriorColor, Color.toVec3Normalized(globalUniforms.uInteriorColor.ref.value, p.interiorColor));
                 }
 
+                if (props.colorMarker !== undefined && props.colorMarker !== p.colorMarker) {
+                    p.colorMarker = props.colorMarker;
+                }
                 if (props.highlightColor !== undefined && props.highlightColor !== p.highlightColor) {
                     p.highlightColor = props.highlightColor;
                     ValueCell.update(globalUniforms.uHighlightColor, Color.toVec3Normalized(globalUniforms.uHighlightColor.ref.value, p.highlightColor));
@@ -705,8 +790,8 @@ namespace Renderer {
                 }
             },
             setViewport: (x: number, y: number, width: number, height: number) => {
-                gl.viewport(x, y, width, height);
-                gl.scissor(x, y, width, height);
+                state.viewport(x, y, width, height);
+                state.scissor(x, y, width, height);
                 if (x !== viewport.x || y !== viewport.y || width !== viewport.width || height !== viewport.height) {
                     Viewport.set(viewport, x, y, width, height);
                     ValueCell.update(globalUniforms.uViewport, Vec4.set(globalUniforms.uViewport.ref.value, x, y, width, height));
@@ -749,4 +834,4 @@ namespace Renderer {
     }
 }
 
-export { Renderer };
\ No newline at end of file
+export { Renderer };
diff --git a/src/mol-gl/scene.ts b/src/mol-gl/scene.ts
index 21f8cd529798cfaf739f8ae3249e5b14ab289cf7..b8a8252b941c13baff32c3951cd8b1193c1f664f 100644
--- a/src/mol-gl/scene.ts
+++ b/src/mol-gl/scene.ts
@@ -45,8 +45,8 @@ function calculateBoundingSphere(renderables: GraphicsRenderable[], boundingSphe
 }
 
 function renderableSort(a: GraphicsRenderable, b: GraphicsRenderable) {
-    const drawProgramIdA = (a.getProgram('colorBlended') || a.getProgram('colorWboit')).id;
-    const drawProgramIdB = (b.getProgram('colorBlended') || a.getProgram('colorWboit')).id;
+    const drawProgramIdA = (a.getProgram('colorBlended') || a.getProgram('colorWboit') || a.getProgram('colorDpoit')).id;
+    const drawProgramIdB = (b.getProgram('colorBlended') || b.getProgram('colorWboit') || b.getProgram('colorDpoit')).id;
     const materialIdA = a.materialId;
     const materialIdB = b.materialId;
 
@@ -80,8 +80,12 @@ interface Scene extends Object3D {
     has: (o: GraphicsRenderObject) => boolean
     clear: () => void
     forEach: (callbackFn: (value: GraphicsRenderable, key: GraphicsRenderObject) => void) => void
+    /** Marker average of primitive renderables */
     readonly markerAverage: number
+    /** Opacity average of primitive renderables */
     readonly opacityAverage: number
+    /** Is `true` if any primitive renderable (possibly) has any opaque part */
+    readonly hasOpaque: boolean
 }
 
 namespace Scene {
@@ -103,6 +107,7 @@ namespace Scene {
 
         let markerAverage = 0;
         let opacityAverage = 0;
+        let hasOpaque = false;
 
         const object3d = Object3D.create();
         const { view, position, direction, up } = object3d;
@@ -160,7 +165,9 @@ namespace Scene {
             }
 
             renderables.sort(renderableSort);
+            markerAverage = calculateMarkerAverage();
             opacityAverage = calculateOpacityAverage();
+            hasOpaque = calculateHasOpaque();
             return true;
         }
 
@@ -182,7 +189,10 @@ namespace Scene {
             const newVisibleHash = computeVisibleHash();
             if (newVisibleHash !== visibleHash) {
                 boundingSphereVisibleDirty = true;
+                markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
+                visibleHash = newVisibleHash;
                 return true;
             } else {
                 return false;
@@ -212,12 +222,27 @@ namespace Scene {
                 // uAlpha is updated in "render" so we need to recompute it here
                 const alpha = clamp(p.values.alpha.ref.value * p.state.alphaFactor, 0, 1);
                 const xray = p.values.dXrayShaded?.ref.value ? 0.5 : 1;
-                opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray;
+                const fuzzy = p.values.dPointStyle?.ref.value === 'fuzzy' ? 0.5 : 1;
+                const text = p.values.dGeometryType.ref.value === 'text' ? 0.5 : 1;
+                opacityAverage += (1 - p.values.transparencyAverage.ref.value) * alpha * xray * fuzzy * text;
                 count += 1;
             }
             return count > 0 ? opacityAverage / count : 0;
         }
 
+        function calculateHasOpaque() {
+            if (primitives.length === 0) return false;
+            for (let i = 0, il = primitives.length; i < il; ++i) {
+                const p = primitives[i];
+                if (!p.state.visible) continue;
+
+                if (p.state.opaque) return true;
+                if (p.state.alphaFactor === 1 && p.values.alpha.ref.value === 1 && p.values.transparencyAverage.ref.value !== 1) return true;
+                if (p.values.dTransparentBackfaces?.ref.value === 'opaque') return true;
+            }
+            return false;
+        }
+
         return {
             view, position, direction, up,
 
@@ -245,6 +270,7 @@ namespace Scene {
                 }
                 markerAverage = calculateMarkerAverage();
                 opacityAverage = calculateOpacityAverage();
+                hasOpaque = calculateHasOpaque();
             },
             add: (o: GraphicsRenderObject) => commitQueue.add(o),
             remove: (o: GraphicsRenderObject) => commitQueue.remove(o),
@@ -281,7 +307,6 @@ namespace Scene {
                 if (boundingSphereVisibleDirty) {
                     calculateBoundingSphere(renderables, boundingSphereVisible, true);
                     boundingSphereVisibleDirty = false;
-                    visibleHash = computeVisibleHash();
                 }
                 return boundingSphereVisible;
             },
@@ -291,6 +316,9 @@ namespace Scene {
             get opacityAverage() {
                 return opacityAverage;
             },
+            get hasOpaque() {
+                return hasOpaque;
+            },
         };
     }
 }
diff --git a/src/mol-gl/shader-code.ts b/src/mol-gl/shader-code.ts
index c65f6df5dd080bcee9075dfabf0275b990f479db..baeb787af1ad4243ccc46f129181c885a9cb407d 100644
--- a/src/mol-gl/shader-code.ts
+++ b/src/mol-gl/shader-code.ts
@@ -67,6 +67,7 @@ import { texture3d_from_1d_trilinear } from './shader/chunks/texture3d-from-1d-t
 import { texture3d_from_2d_linear } from './shader/chunks/texture3d-from-2d-linear.glsl';
 import { texture3d_from_2d_nearest } from './shader/chunks/texture3d-from-2d-nearest.glsl';
 import { wboit_write } from './shader/chunks/wboit-write.glsl';
+import { dpoit_write } from './shader/chunks/dpoit-write.glsl';
 
 const ShaderChunks: { [k: string]: string } = {
     apply_fog,
@@ -99,7 +100,8 @@ const ShaderChunks: { [k: string]: string } = {
     texture3d_from_1d_trilinear,
     texture3d_from_2d_linear,
     texture3d_from_2d_nearest,
-    wboit_write
+    wboit_write,
+    dpoit_write
 };
 
 const reInclude = /^(?!\/\/)\s*#include\s+(\S+)/gm;
@@ -292,7 +294,9 @@ const glsl300VertPrefixCommon = `
 const glsl300FragPrefixCommon = `
 #define varying in
 #define texture2D texture
+#define textureCube texture
 #define texture2DLodEXT textureLod
+#define textureCubeLodEXT textureLod
 
 #define gl_FragColor out_FragData0
 #define gl_FragDepthEXT gl_FragDepth
diff --git a/src/mol-gl/shader/background.frag.ts b/src/mol-gl/shader/background.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..835314ff237890841d3d5c57ffccd73eed72911b
--- /dev/null
+++ b/src/mol-gl/shader/background.frag.ts
@@ -0,0 +1,90 @@
+export const background_frag = `
+precision mediump float;
+precision mediump samplerCube;
+precision mediump sampler2D;
+
+#if defined(dVariant_skybox)
+    uniform samplerCube tSkybox;
+    uniform mat4 uViewDirectionProjectionInverse;
+    uniform float uBlur;
+    uniform float uOpacity;
+    uniform float uSaturation;
+    uniform float uLightness;
+#elif defined(dVariant_image)
+    uniform sampler2D tImage;
+    uniform vec2 uImageScale;
+    uniform vec2 uImageOffset;
+    uniform float uOpacity;
+    uniform float uSaturation;
+    uniform float uLightness;
+#elif defined(dVariant_horizontalGradient) || defined(dVariant_radialGradient)
+    uniform vec3 uGradientColorA;
+    uniform vec3 uGradientColorB;
+    uniform float uGradientRatio;
+#endif
+
+uniform vec2 uTexSize;
+uniform vec4 uViewport;
+uniform bool uViewportAdjusted;
+varying vec4 vPosition;
+
+// TODO: add as general pp option to remove banding?
+// Iestyn's RGB dither from http://alex.vlachos.com/graphics/Alex_Vlachos_Advanced_VR_Rendering_GDC2015.pdf
+vec3 ScreenSpaceDither(vec2 vScreenPos) {
+    vec3 vDither = vec3(dot(vec2(171.0, 231.0), vScreenPos.xy));
+    vDither.rgb = fract(vDither.rgb / vec3(103.0, 71.0, 97.0));
+    return vDither.rgb / 255.0;
+}
+
+vec3 saturateColor(vec3 c, float amount) {
+    // https://www.w3.org/TR/WCAG21/#dfn-relative-luminance
+    const vec3 W = vec3(0.2125, 0.7154, 0.0721);
+    vec3 intensity = vec3(dot(c, W));
+    return mix(intensity, c, 1.0 + amount);
+}
+
+vec3 lightenColor(vec3 c, float amount) {
+    return c + amount;
+}
+
+void main() {
+    #if defined(dVariant_skybox)
+        vec4 t = uViewDirectionProjectionInverse * vPosition;
+        #ifdef enabledShaderTextureLod
+            gl_FragColor = textureCubeLodEXT(tSkybox, normalize(t.xyz / t.w), uBlur * 8.0);
+        #else
+            gl_FragColor = textureCube(tSkybox, normalize(t.xyz / t.w));
+        #endif
+        gl_FragColor.a = uOpacity;
+        gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
+    #elif defined(dVariant_image)
+        vec2 coords;
+        if (uViewportAdjusted) {
+            coords = ((gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uImageScale) + uImageOffset;
+        } else {
+            coords = (gl_FragCoord.xy / uImageScale) + uImageOffset;
+        }
+        gl_FragColor = texture2D(tImage, vec2(coords.x, 1.0 - coords.y));
+        gl_FragColor.a = uOpacity;
+        gl_FragColor.rgb = lightenColor(saturateColor(gl_FragColor.rgb, uSaturation), uLightness);
+    #elif defined(dVariant_horizontalGradient)
+        float d;
+        if (uViewportAdjusted) {
+            d = ((gl_FragCoord.y - uViewport.y) * (uTexSize.y / uViewport.w) / uTexSize.y) + 1.0 - (uGradientRatio * 2.0);
+        } else {
+            d = (gl_FragCoord.y / uTexSize.y) + 1.0 - (uGradientRatio * 2.0);
+        }
+        gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, clamp(d, 0.0, 1.0)), 1.0);
+        gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy);
+    #elif defined(dVariant_radialGradient)
+        float d;
+        if (uViewportAdjusted) {
+            d = distance(vec2(0.5), (gl_FragCoord.xy - uViewport.xy) * (uTexSize / uViewport.zw) / uTexSize) + uGradientRatio - 0.5;
+        } else {
+            d = distance(vec2(0.5), gl_FragCoord.xy / uTexSize) + uGradientRatio - 0.5;
+        }
+        gl_FragColor = vec4(mix(uGradientColorB, uGradientColorA, 1.0 - clamp(d, 0.0, 1.0)), 1.0);
+        gl_FragColor.rgb += ScreenSpaceDither(gl_FragCoord.xy);
+    #endif
+}
+`;
diff --git a/src/mol-gl/shader/background.vert.ts b/src/mol-gl/shader/background.vert.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3f1b86fbbf861b84d577cd73c6e5e1a32908c114
--- /dev/null
+++ b/src/mol-gl/shader/background.vert.ts
@@ -0,0 +1,12 @@
+export const background_vert = `
+precision mediump float;
+
+attribute vec2 aPosition;
+
+varying vec4 vPosition;
+
+void main() {
+    vPosition = vec4(aPosition, 1.0, 1.0);
+    gl_Position = vec4(aPosition, 1.0, 1.0);
+}
+`;
diff --git a/src/mol-gl/shader/blend-back-dpoit.frag.ts b/src/mol-gl/shader/blend-back-dpoit.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0ee166222e68a732f88b71bba6301ecc23151721
--- /dev/null
+++ b/src/mol-gl/shader/blend-back-dpoit.frag.ts
@@ -0,0 +1,20 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ */
+
+export const blendBackDpoit_frag = `
+    precision highp float;
+
+    uniform sampler2D tDpoitBackColor;
+    uniform vec2 uTexSize;
+
+    void main() {
+        vec2 coords = gl_FragCoord.xy / uTexSize;
+        gl_FragColor = texture2D(tDpoitBackColor, coords);
+        if (gl_FragColor.a == 0.0) {
+            discard;
+        }
+    }
+`;
diff --git a/src/mol-gl/shader/chunks/apply-fog.glsl.ts b/src/mol-gl/shader/chunks/apply-fog.glsl.ts
index de2f323eddddde0d96872e7453b5bb8ada0a00ac..f1700d95bee705e85ffe98c029a0ac8cde85325b 100644
--- a/src/mol-gl/shader/chunks/apply-fog.glsl.ts
+++ b/src/mol-gl/shader/chunks/apply-fog.glsl.ts
@@ -12,8 +12,19 @@ if (!uTransparentBackground) {
         gl_FragColor.rgb = mix(gl_FragColor.rgb, uFogColor, fogFactor);
     }
 } else {
-    // pre-multiplied alpha expected for transparent background
-    gl_FragColor.rgb *= fogAlpha;
-    gl_FragColor.a = fogAlpha;
+    #if defined(dRenderVariant_colorDpoit)
+        if (gl_FragColor.a < 1.0) {
+            // transparent objects are blended with background color
+            gl_FragColor.a = fogAlpha;
+        } else {
+            // opaque objects need to be pre-multiplied alpha
+            gl_FragColor.rgb *= fogAlpha;
+            gl_FragColor.a = fogAlpha;
+        }
+    #else
+        // pre-multiplied alpha expected for transparent background
+        gl_FragColor.rgb *= fogAlpha;
+        gl_FragColor.a = fogAlpha;
+    #endif
 }
 `;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/apply-light-color.glsl.ts b/src/mol-gl/shader/chunks/apply-light-color.glsl.ts
index 9b138b9d4f1ac7ab74a0e5a1f785905f3ffc9cf8..bddbf634b013296a098601b85406adb334e5450e 100644
--- a/src/mol-gl/shader/chunks/apply-light-color.glsl.ts
+++ b/src/mol-gl/shader/chunks/apply-light-color.glsl.ts
@@ -13,14 +13,7 @@ export const apply_light_color = `
 #else
     #ifdef bumpEnabled
         if (uBumpFrequency > 0.0 && uBumpAmplitude > 0.0) {
-            vec3 bumpNormal = perturbNormal(-vViewPosition, normal, fbm(vModelPosition * uBumpFrequency), (uBumpAmplitude * bumpiness) / uBumpFrequency);
-            #ifdef enabledFragDepth
-                if (!isNaN(bumpNormal.x) && !isNaN(bumpNormal.y) && !isNaN(bumpNormal.z)) {
-                    normal = bumpNormal;
-                }
-            #else
-                normal = bumpNormal;
-            #endif
+            normal = perturbNormal(-vViewPosition, normal, fbm(vModelPosition * uBumpFrequency), (uBumpAmplitude * bumpiness) / uBumpFrequency);
         }
     #endif
 
@@ -64,6 +57,7 @@ export const apply_light_color = `
     RE_IndirectSpecular_Physical(radiance, iblIrradiance, clearcoatRadiance, geometry, physicalMaterial, reflectedLight);
 
     vec3 outgoingLight = reflectedLight.directDiffuse + reflectedLight.indirectDiffuse + reflectedLight.directSpecular + reflectedLight.indirectSpecular;
+    outgoingLight = clamp(outgoingLight, 0.01, 0.99); // prevents black artifacts on specular highlight with transparent background
 
     gl_FragColor = vec4(outgoingLight, color.a);
 #endif
diff --git a/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts b/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts
index 6f09848c28ae456bf73cf2d39b8895e63fa46c43..2033ca6d93b0cb5aced2ad3ff6f6dd618eb6d3a4 100644
--- a/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts
+++ b/src/mol-gl/shader/chunks/apply-marker-color.glsl.ts
@@ -1,11 +1,13 @@
 export const apply_marker_color = `
-if (marker > 0.0) {
-    if ((uMarkerPriority == 1 && marker != 2.0) || (uMarkerPriority != 1 && marker == 1.0)) {
-        gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength);
-        gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering
-    } else {
-        gl_FragColor.rgb = mix(gl_FragColor.rgb, uSelectColor, uSelectStrength);
-        gl_FragColor.a = max(gl_FragColor.a, uSelectStrength * 0.002); // for direct-volume rendering
+#if defined(dColorMarker)
+    if (marker > 0.0) {
+        if ((uMarkerPriority == 1 && marker != 2.0) || (uMarkerPriority != 1 && marker == 1.0)) {
+            gl_FragColor.rgb = mix(gl_FragColor.rgb, uHighlightColor, uHighlightStrength);
+            gl_FragColor.a = max(gl_FragColor.a, uHighlightStrength * 0.002); // for direct-volume rendering
+        } else {
+            gl_FragColor.rgb = mix(gl_FragColor.rgb, uSelectColor, uSelectStrength);
+            gl_FragColor.a = max(gl_FragColor.a, uSelectStrength * 0.002); // for direct-volume rendering
+        }
     }
-}
+#endif
 `;
\ No newline at end of file
diff --git a/src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts b/src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts
index 361ff36decaa432001944ba7a796b11c99203e02..2548b262849459b8cf038c90e2c18da5f5c6f51b 100644
--- a/src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts
+++ b/src/mol-gl/shader/chunks/assign-marker-varying.glsl.ts
@@ -1,5 +1,5 @@
 export const assign_marker_varying = `
-#if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
+#if defined(dNeedsMarker)
     #if defined(dMarkerType_instance)
         vMarker = readFromTexture(tMarker, aInstance, uMarkerTexDim).a;
     #elif defined(dMarkerType_groupInstance)
diff --git a/src/mol-gl/shader/chunks/assign-material-color.glsl.ts b/src/mol-gl/shader/chunks/assign-material-color.glsl.ts
index 0f206270a5c071ce2a00b501341690acee35b7c3..14b87d16ebd09c59fbaeb372a51c5b74564f04fc 100644
--- a/src/mol-gl/shader/chunks/assign-material-color.glsl.ts
+++ b/src/mol-gl/shader/chunks/assign-material-color.glsl.ts
@@ -1,5 +1,5 @@
 export const assign_material_color = `
-#if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
+#if defined(dNeedsMarker)
     float marker = uMarker;
     if (uMarker == -1.0) {
         marker = floor(vMarker * 255.0 + 0.5); // rounding required to work on some cards on win
@@ -86,7 +86,7 @@ export const assign_material_color = `
 // apply per-group transparency
 #if defined(dTransparency) && (defined(dRenderVariant_pick) || defined(dRenderVariant_color))
     float ta = 1.0 - vTransparency;
-    if (vTransparency < 0.2) ta = 1.0; // hard cutoff looks better
+    if (vTransparency < 0.09) ta = 1.0; // hard cutoff looks better
 
     #if defined(dRenderVariant_pick)
         if (ta < uPickingAlphaThreshold)
diff --git a/src/mol-gl/shader/chunks/color-frag-params.glsl.ts b/src/mol-gl/shader/chunks/color-frag-params.glsl.ts
index 82d1601d20f547069991ce1670db797f7b671ba7..b5802f3688be2243b93853b1556226235d1af0bb 100644
--- a/src/mol-gl/shader/chunks/color-frag-params.glsl.ts
+++ b/src/mol-gl/shader/chunks/color-frag-params.glsl.ts
@@ -27,7 +27,7 @@ uniform float uBumpiness;
         varying vec4 vSubstance;
     #endif
 #elif defined(dRenderVariant_pick)
-    #if __VERSION__ == 100
+    #if __VERSION__ == 100 || !defined(dVaryingGroup)
         #ifdef requiredDrawBuffers
             varying vec4 vObject;
             varying vec4 vInstance;
diff --git a/src/mol-gl/shader/chunks/color-vert-params.glsl.ts b/src/mol-gl/shader/chunks/color-vert-params.glsl.ts
index 5459d2fc31658b1eb257bb67852009c2692bea8d..30a830ad73651982363871cbb3fe8b1fcc20d431 100644
--- a/src/mol-gl/shader/chunks/color-vert-params.glsl.ts
+++ b/src/mol-gl/shader/chunks/color-vert-params.glsl.ts
@@ -55,7 +55,7 @@ uniform float uBumpiness;
         #endif
     #endif
 #elif defined(dRenderVariant_pick)
-    #if __VERSION__ == 100
+    #if __VERSION__ == 100 || !defined(dVaryingGroup)
         #ifdef requiredDrawBuffers
             varying vec4 vObject;
             varying vec4 vInstance;
diff --git a/src/mol-gl/shader/chunks/common-frag-params.glsl.ts b/src/mol-gl/shader/chunks/common-frag-params.glsl.ts
index 877d8663619268257dc392788c63f6beb539a102..79ef4237b771648b1ddd4bd04b53d428232aae8b 100644
--- a/src/mol-gl/shader/chunks/common-frag-params.glsl.ts
+++ b/src/mol-gl/shader/chunks/common-frag-params.glsl.ts
@@ -14,7 +14,7 @@ uniform int uMarkingType;
     uniform vec3 uClipObjectScale[dClipObjectCount];
 
     #if defined(dClipping)
-        #if __VERSION__ == 100
+        #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
             varying float vClipping;
         #else
             flat in float vClipping;
@@ -22,21 +22,29 @@ uniform int uMarkingType;
     #endif
 #endif
 
-uniform vec3 uHighlightColor;
-uniform vec3 uSelectColor;
-uniform float uHighlightStrength;
-uniform float uSelectStrength;
-uniform int uMarkerPriority;
+#if defined(dColorMarker)
+    uniform vec3 uHighlightColor;
+    uniform vec3 uSelectColor;
+    uniform float uHighlightStrength;
+    uniform float uSelectStrength;
+    uniform int uMarkerPriority;
+#endif
 
-#if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
+#if defined(dNeedsMarker)
     uniform float uMarker;
-    #if __VERSION__ == 100
+    #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
         varying float vMarker;
     #else
         flat in float vMarker;
     #endif
 #endif
 
+#if defined(dRenderVariant_colorDpoit)
+    #define MAX_DPOIT_DEPTH 99999.0 // NOTE constant also set in TypeScript
+    uniform sampler2D tDpoitDepth;
+    uniform sampler2D tDpoitFrontColor;
+#endif
+
 varying vec3 vModelPosition;
 varying vec3 vViewPosition;
 
diff --git a/src/mol-gl/shader/chunks/common-vert-params.glsl.ts b/src/mol-gl/shader/chunks/common-vert-params.glsl.ts
index b4e3e2c50c604e079ddf150da5d4a823f276580c..4589093a99daffa056212e269b990bda4e3b3665 100644
--- a/src/mol-gl/shader/chunks/common-vert-params.glsl.ts
+++ b/src/mol-gl/shader/chunks/common-vert-params.glsl.ts
@@ -21,7 +21,7 @@ uniform int uPickType;
     #if defined(dClipping)
         uniform vec2 uClippingTexDim;
         uniform sampler2D tClipping;
-        #if __VERSION__ == 100
+        #if __VERSION__ == 100 || defined(dClippingType_instance) || !defined(dVaryingGroup)
             varying float vClipping;
         #else
             flat out float vClipping;
@@ -29,11 +29,11 @@ uniform int uPickType;
     #endif
 #endif
 
-#if defined(dRenderVariant_color) || defined(dRenderVariant_marking)
+#if defined(dNeedsMarker)
     uniform float uMarker;
     uniform vec2 uMarkerTexDim;
     uniform sampler2D tMarker;
-    #if __VERSION__ == 100
+    #if __VERSION__ == 100 || defined(dMarkerType_instance) || !defined(dVaryingGroup)
         varying float vMarker;
     #else
         flat out float vMarker;
@@ -44,7 +44,9 @@ varying vec3 vModelPosition;
 varying vec3 vViewPosition;
 
 #if defined(noNonInstancedActiveAttribs)
-    #define VertexID gl_VertexID
+    // int() is needed for some Safari versions
+    // see https://bugs.webkit.org/show_bug.cgi?id=244152
+    #define VertexID int(gl_VertexID)
 #else
     attribute float aVertex;
     #define VertexID int(aVertex)
diff --git a/src/mol-gl/shader/chunks/common.glsl.ts b/src/mol-gl/shader/chunks/common.glsl.ts
index 785decd348bc5782b60b354ff47f474d107b1081..49eb5b9dba4027faa2382e7cde8dabdc0757a6b0 100644
--- a/src/mol-gl/shader/chunks/common.glsl.ts
+++ b/src/mol-gl/shader/chunks/common.glsl.ts
@@ -1,7 +1,7 @@
 export const common = `
 // TODO find a better place for these convenience defines
 
-#if defined(dRenderVariant_colorBlended) || defined(dRenderVariant_colorWboit)
+#if defined(dRenderVariant_colorBlended) || defined(dRenderVariant_colorWboit) || defined(dRenderVariant_colorDpoit)
     #define dRenderVariant_color
 #endif
 
@@ -17,6 +17,10 @@ export const common = `
     #define dColorType_varying
 #endif
 
+#if (defined(dRenderVariant_color) && defined(dColorMarker)) || defined(dRenderVariant_marking)
+    #define dNeedsMarker
+#endif
+
 #define MaskAll 0
 #define MaskOpaque 1
 #define MaskTransparent 2
diff --git a/src/mol-gl/shader/chunks/dpoit-write.glsl.ts b/src/mol-gl/shader/chunks/dpoit-write.glsl.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2475d16ce86559a441669bb9b26c127dbf986480
--- /dev/null
+++ b/src/mol-gl/shader/chunks/dpoit-write.glsl.ts
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ */
+
+export const dpoit_write = `
+#if defined(dRenderVariant_colorDpoit)
+    if (uRenderMask == MaskOpaque) {
+        if (preFogAlpha < 1.0) {
+            discard;
+        }
+    } else if (uRenderMask == MaskTransparent) {
+        // the 'fragmentDepth > 0.99' check is to handle precision issues with packed depth
+        vec2 coords = gl_FragCoord.xy / uDrawingBufferSize;
+        if (preFogAlpha != 1.0 && (fragmentDepth < getDepth(coords) || fragmentDepth > 0.99)) {
+            #ifdef dTransparentBackfaces_off
+                if (interior) discard;
+            #endif
+
+            // adapted from https://github.com/tsherif/webgl2examples
+            // The MIT License, Copyright 2017 Tarek Sherif, Shuai Shao
+
+            vec2 lastDepth = texture2D(tDpoitDepth, coords).rg;
+            vec4 lastFrontColor = texture2D(tDpoitFrontColor, coords);
+
+            vec4 fragColor = gl_FragColor;
+
+            // depth value always increases
+            // so we can use MAX blend equation
+            gl_FragData[2].rg = vec2(-MAX_DPOIT_DEPTH);
+
+            // front color always increases
+            // so we can use MAX blend equation
+            gl_FragColor = lastFrontColor;
+
+            // back color is separately blend afterwards each pass
+            gl_FragData[1] = vec4(0.0);
+
+            float nearestDepth = -lastDepth.x;
+            float furthestDepth = lastDepth.y;
+            float alphaMultiplier = 1.0 - lastFrontColor.a;
+
+            if (fragmentDepth < nearestDepth || fragmentDepth > furthestDepth) {
+                // Skip this depth since it's been peeled.
+                return;
+            }
+
+            if (fragmentDepth > nearestDepth && fragmentDepth < furthestDepth) {
+                // This needs to be peeled.
+                // The ones remaining after MAX blended for
+                // all need-to-peel will be peeled next pass.
+                gl_FragData[2].rg = vec2(-fragmentDepth, fragmentDepth);
+                return;
+            }
+
+            // write to back and front color buffer
+            if (fragmentDepth == nearestDepth) {
+                gl_FragColor.rgb += fragColor.rgb * fragColor.a * alphaMultiplier;
+                gl_FragColor.a = 1.0 - alphaMultiplier * (1.0 - fragColor.a);
+            } else {
+                gl_FragData[1] += fragColor;
+            }
+
+        } else {
+            discard;
+        }
+    }
+#endif
+`;
diff --git a/src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts b/src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
index e6c36a65853f3fca7f9f983d2c03ceccf0444179..bec192c1990360a07674dc2bb26d10e6711bd4f1 100644
--- a/src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
+++ b/src/mol-gl/shader/compute/color-smoothing/accumulate.vert.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,7 +10,6 @@ precision highp float;
 #include common
 #include read_from_texture
 
-uniform int uTotalCount;
 uniform int uGroupCount;
 
 attribute float aSample;
diff --git a/src/mol-gl/shader/cylinders.frag.ts b/src/mol-gl/shader/cylinders.frag.ts
index 82c0f8ca738db0fbc2be2ef44cbb9b146e294173..ba17705d7c67d751e744a3f70be36695d6087269 100644
--- a/src/mol-gl/shader/cylinders.frag.ts
+++ b/src/mol-gl/shader/cylinders.frag.ts
@@ -109,14 +109,14 @@ void main() {
 
     vec3 vViewPosition = vModelPosition + intersection.x * rayDir;
     vViewPosition = (uView * vec4(vViewPosition, 1.0)).xyz;
-    gl_FragDepthEXT = calcDepth(vViewPosition);
+    float fragmentDepth = calcDepth(vViewPosition);
 
-    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
+    if (fragmentDepth < 0.0) discard;
+    if (fragmentDepth > 1.0) discard;
 
-    if (gl_FragDepthEXT < 0.0) discard;
-    if (gl_FragDepthEXT > 1.0) discard;
+    gl_FragDepthEXT = fragmentDepth;
 
-    float fragmentDepth = gl_FragDepthEXT;
+    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -142,6 +142,7 @@ void main() {
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/direct-volume.frag.ts b/src/mol-gl/shader/direct-volume.frag.ts
index a6e73a7f3d958ff3b0c192e9444a8bcd14e7ebe5..1b15f590f999549bfda4c25efda6a566a891ccbb 100644
--- a/src/mol-gl/shader/direct-volume.frag.ts
+++ b/src/mol-gl/shader/direct-volume.frag.ts
@@ -50,15 +50,17 @@ uniform int uVertexCount;
 uniform int uInstanceCount;
 uniform int uGroupCount;
 
-uniform vec3 uHighlightColor;
-uniform vec3 uSelectColor;
-uniform float uHighlightStrength;
-uniform float uSelectStrength;
-uniform int uMarkerPriority;
-
-uniform float uMarker;
-uniform vec2 uMarkerTexDim;
-uniform sampler2D tMarker;
+#if defined(dColorMarker)
+    uniform vec3 uHighlightColor;
+    uniform vec3 uSelectColor;
+    uniform float uHighlightStrength;
+    uniform float uSelectStrength;
+    uniform int uMarkerPriority;
+
+    uniform float uMarker;
+    uniform vec2 uMarkerTexDim;
+    uniform sampler2D tMarker;
+#endif
 
 uniform float uMetalness;
 uniform float uRoughness;
@@ -304,11 +306,13 @@ vec4 raymarch(vec3 startLoc, vec3 step, vec3 rayDir) {
 
         gl_FragColor.a = material.a * uAlpha * uTransferScale;
 
-        float marker = uMarker;
-        if (uMarker == -1.0) {
-            marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
-            marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
-        }
+        #if defined(dColorMarker)
+            float marker = uMarker;
+            if (uMarker == -1.0) {
+                marker = readFromTexture(tMarker, vInstance * float(uGroupCount) + group, uMarkerTexDim).a;
+                marker = floor(marker * 255.0 + 0.5); // rounding required to work on some cards on win
+            }
+        #endif
         #include apply_marker_color
 
         preFogAlphaBlended = (1.0 - preFogAlphaBlended) * gl_FragColor.a + preFogAlphaBlended;
@@ -352,4 +356,4 @@ void main() {
     float preFogAlpha = clamp(preFogAlphaBlended, 0.0, 1.0);
     #include wboit_write
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/evaluate-dpoit.frag.ts b/src/mol-gl/shader/evaluate-dpoit.frag.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2b6d6f30da9bcac3bc527b871f056f21f314c662
--- /dev/null
+++ b/src/mol-gl/shader/evaluate-dpoit.frag.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ */
+
+export const evaluateDpoit_frag = `
+precision highp float;
+
+uniform sampler2D tDpoitFrontColor;
+uniform vec2 uTexSize;
+
+void main() {
+    vec2 coords = gl_FragCoord.xy / uTexSize;
+    gl_FragColor = texture2D(tDpoitFrontColor, coords);
+}
+`;
diff --git a/src/mol-gl/shader/image.frag.ts b/src/mol-gl/shader/image.frag.ts
index 39c8835d8c94aee896e484fdbc9149121939f974..b8a147fc13219f7c346f138fd4c9b15d4bd2da99 100644
--- a/src/mol-gl/shader/image.frag.ts
+++ b/src/mol-gl/shader/image.frag.ts
@@ -159,6 +159,7 @@ void main() {
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/lines.frag.ts b/src/mol-gl/shader/lines.frag.ts
index fa62dbf5559b323f98e4e21e1e51d865e6ce900b..1639ae79df6e9d70a78b28a29515a77d7c00bfe5 100644
--- a/src/mol-gl/shader/lines.frag.ts
+++ b/src/mol-gl/shader/lines.frag.ts
@@ -39,6 +39,7 @@ void main(){
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/marching-cubes/isosurface.frag.ts b/src/mol-gl/shader/marching-cubes/isosurface.frag.ts
index ac7f0cf3d8fc48321804bb1a4d99ea23eb4a46b7..964bfdf1753935172219deb41d2593b7479d5c00 100644
--- a/src/mol-gl/shader/marching-cubes/isosurface.frag.ts
+++ b/src/mol-gl/shader/marching-cubes/isosurface.frag.ts
@@ -268,9 +268,9 @@ void main(void) {
     gl_FragData[0].xyz = (uGridTransform * vec4(b0 + t * (b0 - b1), 1.0)).xyz;
 
     // group id
-    #if __VERSION__ == 100
+    #if __VERSION__ == 100 || defined(dConstantGroup)
         // webgl1 does not support 'flat' interpolation (i.e. no interpolation)
-        // so we ensure a constant group id per triangle here
+        // ensure a constant group id per triangle as needed
         #ifdef dPackedGroup
             gl_FragData[1] = vec4(voxel(coord3d).rgb, 1.0);
         #else
diff --git a/src/mol-gl/shader/mesh.frag.ts b/src/mol-gl/shader/mesh.frag.ts
index e49049f60a8f4ddf845476d55321c3bc6aed8f83..98d0ca5445342517fd716e1030df8db3fc2c3960 100644
--- a/src/mol-gl/shader/mesh.frag.ts
+++ b/src/mol-gl/shader/mesh.frag.ts
@@ -62,6 +62,7 @@ void main() {
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/points.frag.ts b/src/mol-gl/shader/points.frag.ts
index 299779b299cbb4049a0b8452c9ae53049c4f9e0a..84001a3d76f59427e3811a227ad0a877e0eb13e4 100644
--- a/src/mol-gl/shader/points.frag.ts
+++ b/src/mol-gl/shader/points.frag.ts
@@ -55,6 +55,7 @@ void main(){
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/spheres.frag.ts b/src/mol-gl/shader/spheres.frag.ts
index e4f2e39e81a8dc4c53bdc6b2d99d285360e936a5..9b3def951a34d227d9211ed14fa835b9aefc5fde 100644
--- a/src/mol-gl/shader/spheres.frag.ts
+++ b/src/mol-gl/shader/spheres.frag.ts
@@ -48,7 +48,6 @@ bool Impostor(out vec3 cameraPos, out vec3 cameraNormal){
 
     cameraPos = rayDirection * negT + rayOrigin;
 
-
     if (calcDepth(cameraPos) <= 0.0) {
         cameraPos = rayDirection * posT + rayOrigin;
         interior = true;
@@ -71,17 +70,17 @@ void main(void){
     }
 
     vec3 vViewPosition = cameraPos;
-    gl_FragDepthEXT = calcDepth(vViewPosition);
-    if (!flag && gl_FragDepthEXT >= 0.0) {
-        gl_FragDepthEXT = 0.0 + (0.0000001 / vRadius);
+    float fragmentDepth = calcDepth(vViewPosition);
+    if (!flag && fragmentDepth >= 0.0) {
+        fragmentDepth = 0.0 + (0.0000001 / vRadius);
     }
 
-    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
+    if (fragmentDepth < 0.0) discard;
+    if (fragmentDepth > 1.0) discard;
 
-    if (gl_FragDepthEXT < 0.0) discard;
-    if (gl_FragDepthEXT > 1.0) discard;
+    gl_FragDepthEXT = fragmentDepth;
 
-    float fragmentDepth = gl_FragDepthEXT;
+    vec3 vModelPosition = (uInvView * vec4(vViewPosition, 1.0)).xyz;
     #include assign_material_color
 
     #if defined(dRenderVariant_pick)
@@ -106,6 +105,7 @@ void main(void){
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/shader/text.frag.ts b/src/mol-gl/shader/text.frag.ts
index ad46e8f3a62a4694cebf872d63f3cc16d27687ba..4e141f1293c1b9933299ac5a0b78d4deb4300546 100644
--- a/src/mol-gl/shader/text.frag.ts
+++ b/src/mol-gl/shader/text.frag.ts
@@ -83,6 +83,7 @@ void main(){
         #include apply_marker_color
         #include apply_fog
         #include wboit_write
+        #include dpoit_write
     #endif
 }
-`;
\ No newline at end of file
+`;
diff --git a/src/mol-gl/webgl/context.ts b/src/mol-gl/webgl/context.ts
index c3cd962a4f42716371db39688dcd4c66cd46ac22..f8d399ae8c71206870a3429fea2b790538f4d96a 100644
--- a/src/mol-gl/webgl/context.ts
+++ b/src/mol-gl/webgl/context.ts
@@ -142,12 +142,12 @@ export function readPixels(gl: GLRenderingContext, x: number, y: number, width:
     if (isDebugMode) checkError(gl);
 }
 
-function getDrawingBufferPixelData(gl: GLRenderingContext) {
+function getDrawingBufferPixelData(gl: GLRenderingContext, state: WebGLState) {
     const w = gl.drawingBufferWidth;
     const h = gl.drawingBufferHeight;
     const buffer = new Uint8Array(w * h * 4);
     unbindFramebuffer(gl);
-    gl.viewport(0, 0, w, h);
+    state.viewport(0, 0, w, h);
     readPixels(gl, 0, 0, w, h, buffer);
     return PixelData.flipY(PixelData.create(buffer, w, h));
 }
@@ -164,6 +164,7 @@ function createStats() {
             renderbuffer: 0,
             shader: 0,
             texture: 0,
+            cubeTexture: 0,
             vertexArray: 0,
         },
 
@@ -345,15 +346,15 @@ export function createContext(gl: GLRenderingContext, props: Partial<{ pixelScal
         readPixelsAsync,
         waitForGpuCommandsComplete: () => waitForGpuCommandsComplete(gl),
         waitForGpuCommandsCompleteSync: () => waitForGpuCommandsCompleteSync(gl),
-        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl),
+        getDrawingBufferPixelData: () => getDrawingBufferPixelData(gl, state),
         clear: (red: number, green: number, blue: number, alpha: number) => {
             unbindFramebuffer(gl);
             state.enable(gl.SCISSOR_TEST);
             state.depthMask(true);
             state.colorMask(true, true, true, true);
             state.clearColor(red, green, blue, alpha);
-            gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
-            gl.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
+            state.scissor(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight);
             gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
         },
 
diff --git a/src/mol-gl/webgl/render-item.ts b/src/mol-gl/webgl/render-item.ts
index f25090291d9a891331e313133817586396949d7a..47d62df93bc773b8bdae262181e4254eac6c505e 100644
--- a/src/mol-gl/webgl/render-item.ts
+++ b/src/mol-gl/webgl/render-item.ts
@@ -1,11 +1,12 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { createAttributeBuffers, ElementsBuffer, AttributeKind } from './buffer';
-import { createTextures, Texture, Textures } from './texture';
+import { createTextures, Texture } from './texture';
 import { WebGLContext, checkError } from './context';
 import { ShaderCode, DefineValues } from '../shader-code';
 import { Program } from './program';
@@ -42,18 +43,19 @@ export interface RenderItem<T extends string> {
     readonly materialId: number
     getProgram: (variant: T) => Program
 
-    render: (variant: T, sharedTexturesList?: Textures) => void
+    render: (variant: T, sharedTexturesCount: number) => void
     update: () => Readonly<ValueChanges>
     destroy: () => void
 }
 
 //
 
-const GraphicsRenderVariant = { colorBlended: '', colorWboit: '', pick: '', depth: '', marking: '' };
+const GraphicsRenderVariant = { colorBlended: '', colorWboit: '', colorDpoit: '', pick: '', depth: '', marking: '' };
 export type GraphicsRenderVariant = keyof typeof GraphicsRenderVariant
 export const GraphicsRenderVariants = Object.keys(GraphicsRenderVariant) as GraphicsRenderVariant[];
-export const GraphicsRenderVariantsBlended = GraphicsRenderVariants.filter(v => v !== 'colorWboit');
-export const GraphicsRenderVariantsWboit = GraphicsRenderVariants.filter(v => v !== 'colorBlended');
+export const GraphicsRenderVariantsBlended = GraphicsRenderVariants.filter(v => !['colorWboit', 'colorDpoit'].includes(v));
+export const GraphicsRenderVariantsWboit = GraphicsRenderVariants.filter(v => !['colorBlended', 'colorDpoit'].includes(v));
+export const GraphicsRenderVariantsDpoit = GraphicsRenderVariants.filter(v => !['colorWboit', 'colorBlended'].includes(v));
 
 const ComputeRenderVariant = { compute: '' };
 export type ComputeRenderVariant = keyof typeof ComputeRenderVariant
@@ -118,7 +120,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         (schema as any).aVertex = AttributeSpec('float32', 1, 0);
     }
 
-    const { attributeValues, defineValues, textureValues, uniformValues, materialUniformValues, bufferedUniformValues } = splitValues(schema, values);
+    const { attributeValues, defineValues, textureValues, materialTextureValues, uniformValues, materialUniformValues, bufferedUniformValues } = splitValues(schema, values);
 
     const uniformValueEntries = Object.entries(uniformValues);
     const materialUniformValueEntries = Object.entries(materialUniformValues);
@@ -136,6 +138,7 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
     }
 
     const textures = createTextures(ctx, schema, textureValues);
+    const materialTextures = createTextures(ctx, schema, materialTextureValues);
     const attributeBuffers = createAttributeBuffers(ctx, schema, attributeValues);
 
     let elementsBuffer: ElementsBuffer | undefined;
@@ -149,8 +152,8 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         vertexArrays[k] = vertexArrayObject ? resources.vertexArray(programs[k], attributeBuffers, elementsBuffer) : null;
     }
 
-    let drawCount = values.drawCount.ref.value;
-    let instanceCount = values.instanceCount.ref.value;
+    let drawCount: number = values.drawCount.ref.value;
+    let instanceCount: number = values.instanceCount.ref.value;
 
     stats.drawCount += drawCount;
     stats.instanceCount += instanceCount;
@@ -166,17 +169,12 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
         materialId,
         getProgram: (variant: T) => programs[variant],
 
-        render: (variant: T, sharedTexturesList?: Textures) => {
-            if (drawCount === 0 || instanceCount === 0 || ctx.isContextLost) return;
+        render: (variant: T, sharedTexturesCount: number) => {
+            if (drawCount === 0 || instanceCount === 0) return;
             const program = programs[variant];
             if (program.id === currentProgramId && state.currentRenderItemId === id) {
                 program.setUniforms(uniformValueEntries);
-                if (sharedTexturesList && sharedTexturesList.length > 0) {
-                    program.bindTextures(sharedTexturesList, 0);
-                    program.bindTextures(textures, sharedTexturesList.length);
-                } else {
-                    program.bindTextures(textures, 0);
-                }
+                program.bindTextures(textures, sharedTexturesCount);
             } else {
                 const vertexArray = vertexArrays[variant];
                 if (program.id !== state.currentProgramId || program.id !== currentProgramId ||
@@ -185,17 +183,13 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                     // console.log('program.id changed or materialId changed/-1', materialId)
                     if (program.id !== state.currentProgramId) program.use();
                     program.setUniforms(materialUniformValueEntries);
+                    program.bindTextures(materialTextures, sharedTexturesCount + textures.length);
                     state.currentMaterialId = materialId;
                     currentProgramId = program.id;
                 }
                 program.setUniforms(uniformValueEntries);
                 program.setUniforms(frontBufferUniformValueEntries);
-                if (sharedTexturesList && sharedTexturesList.length > 0) {
-                    program.bindTextures(sharedTexturesList, 0);
-                    program.bindTextures(textures, sharedTexturesList.length);
-                } else {
-                    program.bindTextures(textures, 0);
-                }
+                program.bindTextures(textures, sharedTexturesCount);
                 if (vertexArray) {
                     vertexArray.bind();
                     // need to bind elements buffer explicitly since it is not always recorded in the VAO
@@ -324,6 +318,22 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
                 }
             }
 
+            for (let i = 0, il = materialTextures.length; i < il; ++i) {
+                const [k, texture] = materialTextures[i];
+                const value = materialTextureValues[k];
+                if (value.ref.version !== versions[k]) {
+                    // update of textures with kind 'texture' is done externally
+                    if (schema[k].kind !== 'texture') {
+                        // console.log('texture version changed, uploading image', k);
+                        texture.load(value.ref.value as TextureImage<any> | TextureVolume<any>);
+                        valueChanges.textures = true;
+                    } else {
+                        materialTextures[i][1] = value.ref.value as Texture;
+                    }
+                    versions[k] = value.ref.version;
+                }
+            }
+
             for (let i = 0, il = backBufferUniformValueEntries.length; i < il; ++i) {
                 const [k, uniform] = backBufferUniformValueEntries[i];
                 if (uniform.ref.version !== versions[k]) {
@@ -359,4 +369,4 @@ export function createRenderItem<T extends string>(ctx: WebGLContext, drawMode:
             }
         }
     };
-}
\ No newline at end of file
+}
diff --git a/src/mol-gl/webgl/resources.ts b/src/mol-gl/webgl/resources.ts
index 4dd175cc7788bfdffd80614b8a56add7d2f7a336..3e2e2d370bfc771499cf8b5b7641f5ab25b2ce52 100644
--- a/src/mol-gl/webgl/resources.ts
+++ b/src/mol-gl/webgl/resources.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -17,7 +17,7 @@ import { hashString, hashFnv32a } from '../../mol-data/util';
 import { DefineValues, ShaderCode } from '../shader-code';
 import { RenderableSchema } from '../renderable/schema';
 import { createRenderbuffer, Renderbuffer, RenderbufferAttachment, RenderbufferFormat } from './renderbuffer';
-import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture } from './texture';
+import { Texture, TextureKind, TextureFormat, TextureType, TextureFilter, createTexture, CubeFaces, createCubeTexture } from './texture';
 import { VertexArray, createVertexArray } from './vertex-array';
 
 function defineValueHash(v: boolean | number | string): number {
@@ -59,6 +59,7 @@ export interface WebGLResources {
     renderbuffer: (format: RenderbufferFormat, attachment: RenderbufferAttachment, width: number, height: number) => Renderbuffer
     shader: (type: ShaderType, source: string) => Shader
     texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => Texture,
+    cubeTexture: (faces: CubeFaces, mipaps: boolean, onload?: () => void) => Texture,
     vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => VertexArray,
 
     getByteCounts: () => ByteCounts
@@ -76,6 +77,7 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
         renderbuffer: new Set<Resource>(),
         shader: new Set<Resource>(),
         texture: new Set<Resource>(),
+        cubeTexture: new Set<Resource>(),
         vertexArray: new Set<Resource>(),
     };
 
@@ -137,6 +139,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
         texture: (kind: TextureKind, format: TextureFormat, type: TextureType, filter: TextureFilter) => {
             return wrap('texture', createTexture(gl, extensions, kind, format, type, filter));
         },
+        cubeTexture: (faces: CubeFaces, mipmaps: boolean, onload?: () => void) => {
+            return wrap('cubeTexture', createCubeTexture(gl, faces, mipmaps, onload));
+        },
         vertexArray: (program: Program, attributeBuffers: AttributeBuffers, elementsBuffer?: ElementsBuffer) => {
             return wrap('vertexArray', createVertexArray(gl, extensions, program, attributeBuffers, elementsBuffer));
         },
@@ -146,6 +151,9 @@ export function createResources(gl: GLRenderingContext, state: WebGLState, stats
             sets.texture.forEach(r => {
                 texture += (r as Texture).getByteCount();
             });
+            sets.cubeTexture.forEach(r => {
+                texture += (r as Texture).getByteCount();
+            });
 
             let attribute = 0;
             sets.attribute.forEach(r => {
diff --git a/src/mol-gl/webgl/state.ts b/src/mol-gl/webgl/state.ts
index d84c91bc8fd48ed129b996d74dfdada51f7b958c..dc6184d7e894b8d274a4c111a1fc8355436b1e88 100644
--- a/src/mol-gl/webgl/state.ts
+++ b/src/mol-gl/webgl/state.ts
@@ -69,6 +69,9 @@ export type WebGLState = {
     clearVertexAttribsState: () => void
     disableUnusedVertexAttribs: () => void
 
+    viewport: (x: number, y: number, width: number, height: number) => void
+    scissor: (x: number, y: number, width: number, height: number) => void
+
     reset: () => void
 }
 
@@ -95,6 +98,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
     let maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
     const vertexAttribsState: number[] = [];
 
+    let currentViewport: [number, number, number, number] = gl.getParameter(gl.VIEWPORT);
+    let currentScissor: [number, number, number, number] = gl.getParameter(gl.SCISSOR_BOX);
+
     const clearVertexAttribsState = () => {
         for (let i = 0; i < maxVertexAttribs; ++i) {
             vertexAttribsState[i] = 0;
@@ -222,6 +228,26 @@ export function createState(gl: GLRenderingContext): WebGLState {
             }
         },
 
+        viewport: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentViewport[0] || y !== currentViewport[1] || width !== currentViewport[2] || height !== currentViewport[3]) {
+                gl.viewport(x, y, width, height);
+                currentViewport[0] = x;
+                currentViewport[1] = y;
+                currentViewport[2] = width;
+                currentViewport[3] = height;
+            }
+        },
+
+        scissor: (x: number, y: number, width: number, height: number) => {
+            if (x !== currentScissor[0] || y !== currentScissor[1] || width !== currentScissor[2] || height !== currentScissor[3]) {
+                gl.scissor(x, y, width, height);
+                currentScissor[0] = x;
+                currentScissor[1] = y;
+                currentScissor[2] = width;
+                currentScissor[3] = height;
+            }
+        },
+
         reset: () => {
             enabledCapabilities = {};
 
@@ -247,6 +273,9 @@ export function createState(gl: GLRenderingContext): WebGLState {
             for (let i = 0; i < maxVertexAttribs; ++i) {
                 vertexAttribsState[i] = 0;
             }
+
+            currentViewport = gl.getParameter(gl.VIEWPORT);
+            currentScissor = gl.getParameter(gl.SCISSOR_BOX);
         }
     };
 }
\ No newline at end of file
diff --git a/src/mol-gl/webgl/texture.ts b/src/mol-gl/webgl/texture.ts
index 3966a268d5d2d96cc7abbc94c18013523b86e617..d12c95d1c5ad65d52202c3ca14470c8931ac8603 100644
--- a/src/mol-gl/webgl/texture.ts
+++ b/src/mol-gl/webgl/texture.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { WebGLContext } from './context';
@@ -11,8 +12,9 @@ import { RenderableSchema } from '../renderable/schema';
 import { idFactory } from '../../mol-util/id-factory';
 import { Framebuffer } from './framebuffer';
 import { isWebGL2, GLRenderingContext } from './compat';
-import { ValueOf } from '../../mol-util/type-helpers';
+import { isPromiseLike, ValueOf } from '../../mol-util/type-helpers';
 import { WebGLExtensions } from './extensions';
+import { objectForEach } from '../../mol-util/object';
 
 const getNextTextureId = idFactory();
 
@@ -30,7 +32,7 @@ export type TextureKindValue = {
 export type TextureValueType = ValueOf<TextureKindValue>
 export type TextureKind = keyof TextureKindValue
 export type TextureType = 'ubyte' | 'ushort' | 'float' | 'fp16' | 'int'
-export type TextureFormat = 'alpha' | 'rgb' | 'rgba' | 'depth'
+export type TextureFormat = 'alpha' | 'rg' | 'rgb' | 'rgba' | 'depth'
 /** Numbers are shortcuts for color attachment */
 export type TextureAttachment = 'depth' | 'stencil' | 'color0' | 'color1' | 'color2' | 'color3' | 'color4' | 'color5' | 'color6' | 'color7' | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
 export type TextureFilter = 'nearest' | 'linear'
@@ -62,6 +64,10 @@ export function getFormat(gl: GLRenderingContext, format: TextureFormat, type: T
         case 'rgb':
             if (isWebGL2(gl) && type === 'int') return gl.RGB_INTEGER;
             return gl.RGB;
+        case 'rg':
+            if (isWebGL2(gl) && type === 'float') return gl.RG;
+            else if (isWebGL2(gl) && type === 'int') return gl.RG_INTEGER;
+            else throw new Error('texture format "rg" requires webgl2 and type "float" or int"');
         case 'rgba':
             if (isWebGL2(gl) && type === 'int') return gl.RGBA_INTEGER;
             return gl.RGBA;
@@ -79,6 +85,13 @@ export function getInternalFormat(gl: GLRenderingContext, format: TextureFormat,
                     case 'fp16': return gl.R16F;
                     case 'int': return gl.R32I;
                 }
+            case 'rg':
+                switch (type) {
+                    case 'ubyte': return gl.RG;
+                    case 'float': return gl.RG32F;
+                    case 'fp16': return gl.RG16F;
+                    case 'int': return gl.RG32I;
+                }
             case 'rgb':
                 switch (type) {
                     case 'ubyte': return gl.RGB;
@@ -111,6 +124,7 @@ function getByteCount(format: TextureFormat, type: TextureType, width: number, h
 function getFormatSize(format: TextureFormat) {
     switch (format) {
         case 'alpha': return 1;
+        case 'rg': return 2;
         case 'rgb': return 3;
         case 'rgba': return 4;
         case 'depth': return 4;
@@ -423,6 +437,123 @@ export function loadImageTexture(src: string, cell: ValueCell<Texture>, texture:
 
 //
 
+export type CubeSide = 'nx' | 'ny' | 'nz' | 'px' | 'py' | 'pz';
+
+export type CubeFaces = {
+    [k in CubeSide]: string | File | Promise<Blob>;
+}
+
+export function getCubeTarget(gl: GLRenderingContext, side: CubeSide): number {
+    switch (side) {
+        case 'nx': return gl.TEXTURE_CUBE_MAP_NEGATIVE_X;
+        case 'ny': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Y;
+        case 'nz': return gl.TEXTURE_CUBE_MAP_NEGATIVE_Z;
+        case 'px': return gl.TEXTURE_CUBE_MAP_POSITIVE_X;
+        case 'py': return gl.TEXTURE_CUBE_MAP_POSITIVE_Y;
+        case 'pz': return gl.TEXTURE_CUBE_MAP_POSITIVE_Z;
+    }
+}
+
+export function createCubeTexture(gl: GLRenderingContext, faces: CubeFaces, mipmaps: boolean, onload?: (errored?: boolean) => void): Texture {
+    const target = gl.TEXTURE_CUBE_MAP;
+    const filter = gl.LINEAR;
+    const internalFormat = gl.RGBA;
+    const format = gl.RGBA;
+    const type = gl.UNSIGNED_BYTE;
+
+    let size = 0;
+
+    const texture = gl.createTexture();
+    gl.bindTexture(target, texture);
+
+    let loadedCount = 0;
+    objectForEach(faces, (source, side) => {
+        if (!source) return;
+
+        const level = 0;
+        const cubeTarget = getCubeTarget(gl, side as CubeSide);
+
+        const image = new Image();
+        if (source instanceof File) {
+            image.src = URL.createObjectURL(source);
+        } else if (isPromiseLike(source)) {
+            source.then(blob => {
+                image.src = URL.createObjectURL(blob);
+            });
+        } else {
+            image.src = source;
+        }
+        image.addEventListener('load', () => {
+            if (size === 0) size = image.width;
+
+            gl.texImage2D(cubeTarget, level, internalFormat, size, size, 0, format, type, null);
+            gl.pixelStorei(gl.UNPACK_ALIGNMENT, 4);
+            gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE);
+            gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0);
+            gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);
+            gl.bindTexture(target, texture);
+            gl.texImage2D(cubeTarget, level, internalFormat, format, type, image);
+
+            loadedCount += 1;
+            if (loadedCount === 6) {
+                if (!destroyed) {
+                    if (mipmaps) {
+                        gl.generateMipmap(target);
+                        gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, gl.LINEAR_MIPMAP_LINEAR);
+                    } else {
+                        gl.texParameteri(target, gl.TEXTURE_MIN_FILTER, filter);
+                    }
+                    gl.texParameteri(target, gl.TEXTURE_MAG_FILTER, filter);
+                }
+                onload?.(destroyed);
+            }
+        });
+        image.addEventListener('error', () => {
+            onload?.(true);
+        });
+    });
+
+    let destroyed = false;
+
+    return {
+        id: getNextTextureId(),
+        target,
+        format,
+        internalFormat,
+        type,
+        filter,
+
+        getWidth: () => size,
+        getHeight: () => size,
+        getDepth: () => 0,
+        getByteCount: () => {
+            return getByteCount('rgba', 'ubyte', size, size, 0) * 6 * (mipmaps ? 2 : 1);
+        },
+
+        define: () => {},
+        load: () => {},
+        bind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, texture);
+        },
+        unbind: (id: TextureId) => {
+            gl.activeTexture(gl.TEXTURE0 + id);
+            gl.bindTexture(target, null);
+        },
+        attachFramebuffer: () => {},
+        detachFramebuffer: () => {},
+
+        reset: () => {},
+        destroy: () => {
+            if (destroyed) return;
+            gl.deleteTexture(texture);
+            destroyed = true;
+        },
+    };
+}
+
+//
+
 export function createNullTexture(gl?: GLRenderingContext): Texture {
     const target = gl?.TEXTURE_2D ?? 3553;
     return {
diff --git a/src/mol-io/common/binary-cif/array-encoder.ts b/src/mol-io/common/binary-cif/array-encoder.ts
index 08dfee8f6d315a0c995cdc0939f7d76648714365..7ca14219f7777ca448e5b5ae04615e7f07205023 100644
--- a/src/mol-io/common/binary-cif/array-encoder.ts
+++ b/src/mol-io/common/binary-cif/array-encoder.ts
@@ -264,28 +264,35 @@ export namespace ArrayEncoding {
         return false;
     }
 
-    function packingSize(data: Int32Array, upperLimit: number) {
+    function packingSizeUnsigned(data: Int32Array, upperLimit: number) {
+        let size = 0;
+        for (let i = 0, n = data.length; i < n; i++) {
+            size += (data[i] / upperLimit) | 0;
+        }
+        size += data.length;
+        return size;
+    }
+
+
+    function packingSizeSigned(data: Int32Array, upperLimit: number) {
         const lowerLimit = -upperLimit - 1;
         let size = 0;
         for (let i = 0, n = data.length; i < n; i++) {
             const value = data[i];
-            if (value === 0) {
-                size += 1;
-            } else if (value > 0) {
-                size += Math.ceil(value / upperLimit);
-                if (value % upperLimit === 0) size += 1;
+            if (value >= 0) {
+                size += (value / upperLimit) | 0;
             } else {
-                size += Math.ceil(value / lowerLimit);
-                if (value % lowerLimit === 0) size += 1;
+                size += (value / lowerLimit) | 0;
             }
         }
+        size += data.length;
         return size;
     }
 
     function determinePacking(data: Int32Array): { isSigned: boolean, size: number, bytesPerElement: number } {
         const signed = isSigned(data);
-        const size8 = signed ? packingSize(data, 0x7F) : packingSize(data, 0xFF);
-        const size16 = signed ? packingSize(data, 0x7FFF) : packingSize(data, 0xFFFF);
+        const size8 = signed ? packingSizeSigned(data, 0x7F) : packingSizeUnsigned(data, 0xFF);
+        const size16 = signed ? packingSizeSigned(data, 0x7FFF) : packingSizeUnsigned(data, 0xFFFF);
 
         if (data.length * 4 < size16 * 2) {
             // 4 byte packing is the most effective
diff --git a/src/mol-io/reader/cif/schema/bird.ts b/src/mol-io/reader/cif/schema/bird.ts
index 5d88c181affb0c14c762e23046174d3a1078b3ae..882751de4acff8ebed74b3e426dbc4f59ee924d0 100644
--- a/src/mol-io/reader/cif/schema/bird.ts
+++ b/src/mol-io/reader/cif/schema/bird.ts
@@ -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.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.362, IHM 1.17, MA 1.4.3.
  *
  * @author molstar/ciftools package
  */
diff --git a/src/mol-io/reader/cif/schema/ccd.ts b/src/mol-io/reader/cif/schema/ccd.ts
index 86ea246065cc2bb1cbdf248cb1f815311648be8f..abc49e51dc51aaf171cb31f71ae68ca4197c3d2b 100644
--- a/src/mol-io/reader/cif/schema/ccd.ts
+++ b/src/mol-io/reader/cif/schema/ccd.ts
@@ -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.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.362, IHM 1.17, MA 1.4.3.
  *
  * @author molstar/ciftools package
  */
diff --git a/src/mol-io/reader/cif/schema/mmcif.ts b/src/mol-io/reader/cif/schema/mmcif.ts
index 51d060ddc0fd8cf5076fdcaac37712256978edbd..e39a178b6efb373b97f356c6f95dd134a97f6367 100644
--- a/src/mol-io/reader/cif/schema/mmcif.ts
+++ b/src/mol-io/reader/cif/schema/mmcif.ts
@@ -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.359, IHM 1.17, MA 1.4.1.
+ * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.362, IHM 1.17, MA 1.4.3.
  *
  * @author molstar/ciftools package
  */
diff --git a/src/mol-math/geometry/_spec/lookup3d.spec.ts b/src/mol-math/geometry/_spec/lookup3d.spec.ts
index 1641a2c6e85a431a265c4d8708baf453976b872a..9b0fe2d35f617048544926f20f6e4e61416aa308 100644
--- a/src/mol-math/geometry/_spec/lookup3d.spec.ts
+++ b/src/mol-math/geometry/_spec/lookup3d.spec.ts
@@ -2,6 +2,7 @@
  * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { GridLookup3D } from '../../geometry';
@@ -24,9 +25,17 @@ describe('GridLookup3d', () => {
         expect(r.count).toBe(1);
         expect(r.indices[0]).toBe(0);
 
+        r = grid.nearest(0, 0, 0, 1);
+        expect(r.count).toBe(1);
+        expect(r.indices[0]).toBe(0);
+
         r = grid.find(0, 0, 0, 1);
         expect(r.count).toBe(3);
         expect(sortArray(r.indices)).toEqual([0, 1, 2]);
+
+        r = grid.nearest(0, 0, 0, 3);
+        expect(r.count).toBe(3);
+        expect(sortArray(r.indices)).toEqual([0, 1, 2]);
     });
 
     it('radius', () => {
@@ -38,9 +47,17 @@ describe('GridLookup3d', () => {
         expect(r.count).toBe(1);
         expect(r.indices[0]).toBe(0);
 
+        r = grid.nearest(0, 0, 0, 1);
+        expect(r.count).toBe(1);
+        expect(r.indices[0]).toBe(0);
+
         r = grid.find(0, 0, 0, 0.5);
         expect(r.count).toBe(2);
         expect(sortArray(r.indices)).toEqual([0, 1]);
+
+        r = grid.nearest(0, 0, 0, 3);
+        expect(r.count).toBe(3);
+        expect(sortArray(r.indices)).toEqual([0, 1, 2]);
     });
 
     it('indexed', () => {
@@ -51,8 +68,15 @@ describe('GridLookup3d', () => {
         let r = grid.find(0, 0, 0, 0);
         expect(r.count).toBe(0);
 
+        r = grid.nearest(0, 0, 0, 1);
+        expect(r.count).toBe(1);
+
         r = grid.find(0, 0, 0, 0.5);
         expect(r.count).toBe(1);
         expect(sortArray(r.indices)).toEqual([0]);
+
+        r = grid.nearest(0, 0, 0, 3);
+        expect(r.count).toBe(1);
+        expect(sortArray(r.indices)).toEqual([0]);
     });
-});
\ No newline at end of file
+});
diff --git a/src/mol-math/geometry/gaussian-density/gpu.ts b/src/mol-math/geometry/gaussian-density/gpu.ts
index 23f7612d6b62f0ff35760f54b093e03ab706a828..a9621f6e21d8c74371fec6a6a7d03d28009315ae 100644
--- a/src/mol-math/geometry/gaussian-density/gpu.ts
+++ b/src/mol-math/geometry/gaussian-density/gpu.ts
@@ -42,7 +42,7 @@ const GaussianDensitySchema = {
     uAlpha: UniformSpec('f', 'material'),
     uResolution: UniformSpec('f', 'material'),
     uRadiusFactorInv: UniformSpec('f', 'material'),
-    tMinDistanceTex: TextureSpec('texture', 'rgba', 'float', 'nearest'),
+    tMinDistanceTex: TextureSpec('texture', 'rgba', 'float', 'nearest', 'material'),
 
     dGridTexType: DefineSpec('string', ['2d', '3d']),
     dCalcType: DefineSpec('string', ['density', 'minDistance', 'groupId']),
@@ -166,8 +166,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
         state.currentRenderItemId = -1;
         fbTex.attachFramebuffer(framebuffer, 0);
         if (clear) {
-            gl.viewport(0, 0, width, height);
-            gl.scissor(0, 0, width, height);
+            state.viewport(0, 0, width, height);
+            state.scissor(0, 0, width, height);
             gl.clear(gl.COLOR_BUFFER_BIT);
         }
         ValueCell.update(uCurrentY, 0);
@@ -184,8 +184,8 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
             // console.log({ i, currX, currY });
             ValueCell.update(uCurrentX, currX);
             ValueCell.update(uCurrentSlice, i);
-            gl.viewport(currX, currY, dx, dy);
-            gl.scissor(currX, currY, dx, dy);
+            state.viewport(currX, currY, dx, dy);
+            state.scissor(currX, currY, dx, dy);
             renderable.render();
             ++currCol;
             currX += dx;
@@ -204,7 +204,7 @@ function calcGaussianDensityTexture2d(webgl: WebGLContext, position: PositionDat
         render(texture, false);
     }
 
-    // printTexture(webgl, minDistTex, 0.75);
+    // printTextureImage(readTexture(webgl, minDistTex), { scale: 0.75 });
 
     return { texture, scale, bbox: expandedBox, gridDim: dim, gridTexDim, gridTexScale, radiusFactor, resolution, maxRadius };
 }
@@ -232,8 +232,8 @@ function calcGaussianDensityTexture3d(webgl: WebGLContext, position: PositionDat
     const framebuffer = getFramebuffer(webgl);
     framebuffer.bind();
     setRenderingDefaults(webgl);
-    gl.viewport(0, 0, dx, dy);
-    gl.scissor(0, 0, dx, dy);
+    state.viewport(0, 0, dx, dy);
+    state.scissor(0, 0, dx, dy);
 
     if (!texture) texture = colorBufferHalfFloat && textureHalfFloat
         ? resources.texture('volume-float16', 'rgba', 'fp16', 'linear')
diff --git a/src/mol-math/geometry/lookup3d/common.ts b/src/mol-math/geometry/lookup3d/common.ts
index 24b457d77d5207956cf997c1bd346e5dc8af6ade..2c6c76cc2635963617d02ac10d6f5fa47e9ca0f3 100644
--- a/src/mol-math/geometry/lookup3d/common.ts
+++ b/src/mol-math/geometry/lookup3d/common.ts
@@ -41,8 +41,9 @@ export namespace Result {
 export interface Lookup3D<T = number> {
     // The result is mutated with each call to find.
     find(x: number, y: number, z: number, radius: number, result?: Result<T>): Result<T>,
+    nearest(x: number, y: number, z: number, k: number, stopIf?: Function, result?: Result<T>): Result<T>,
     check(x: number, y: number, z: number, radius: number): boolean,
     readonly boundary: { readonly box: Box3D, readonly sphere: Sphere3D }
     /** transient result */
     readonly result: Result<T>
-}
\ No newline at end of file
+}
diff --git a/src/mol-math/geometry/lookup3d/grid.ts b/src/mol-math/geometry/lookup3d/grid.ts
index 970cce5881913c9d5447ca613dba7b26619b5378..b4f96dd0f9aee2c8bc1c4aaec740ed3e5fbbde03 100644
--- a/src/mol-math/geometry/lookup3d/grid.ts
+++ b/src/mol-math/geometry/lookup3d/grid.ts
@@ -3,6 +3,7 @@
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { Result, Lookup3D } from './common';
@@ -12,6 +13,7 @@ import { PositionData } from '../common';
 import { Vec3 } from '../../linear-algebra';
 import { OrderedSet } from '../../../mol-data/int';
 import { Boundary } from '../boundary';
+import { FibonacciHeap } from '../../../mol-util/fibonacci-heap';
 
 interface GridLookup3D<T = number> extends Lookup3D<T> {
     readonly buckets: { readonly offset: ArrayLike<number>, readonly count: ArrayLike<number>, readonly array: ArrayLike<number> }
@@ -40,6 +42,17 @@ class GridLookup3DImpl<T extends number = number> implements GridLookup3D<T> {
         return ret;
     }
 
+    nearest(x: number, y: number, z: number, k: number = 1, stopIf?: Function, result?: Result<T>): Result<T> {
+        this.ctx.x = x;
+        this.ctx.y = y;
+        this.ctx.z = z;
+        this.ctx.k = k;
+        this.ctx.stopIf = stopIf;
+        const ret = result ?? this.result;
+        queryNearest(this.ctx, ret);
+        return ret;
+    }
+
     check(x: number, y: number, z: number, radius: number): boolean {
         this.ctx.x = x;
         this.ctx.y = y;
@@ -221,12 +234,14 @@ interface QueryContext {
     x: number,
     y: number,
     z: number,
+    k: number,
+    stopIf?: Function,
     radius: number,
     isCheck: boolean
 }
 
 function createContext(grid: Grid3D): QueryContext {
-    return { grid, x: 0.1, y: 0.1, z: 0.1, radius: 0.1, isCheck: false };
+    return { grid, x: 0.1, y: 0.1, z: 0.1, k: 1, stopIf: undefined, radius: 0.1, isCheck: false };
 }
 
 function query<T extends number = number>(ctx: QueryContext, result: Result<T>): boolean {
@@ -277,4 +292,152 @@ function query<T extends number = number>(ctx: QueryContext, result: Result<T>):
         }
     }
     return result.count > 0;
-}
\ No newline at end of file
+}
+
+const tmpDirVec = Vec3();
+const tmpVec = Vec3();
+const tmpSetG = new Set<number>();
+const tmpSetG2 = new Set<number>();
+const tmpArrG1 = [0.1];
+const tmpArrG2 = [0.1];
+const tmpArrG3 = [0.1];
+const tmpHeapG = new FibonacciHeap();
+function queryNearest<T extends number = number>(ctx: QueryContext, result: Result<T>): boolean {
+    const { min, expandedBox: box, boundingSphere: { center }, size: [sX, sY, sZ], bucketOffset, bucketCounts, bucketArray, grid, data: { x: px, y: py, z: pz, indices, radius }, delta, maxRadius } = ctx.grid;
+    const { x, y, z, k, stopIf } = ctx;
+    const indicesCount = OrderedSet.size(indices);
+    Result.reset(result);
+    if (indicesCount === 0 || k <= 0) return false;
+    let gX, gY, gZ, stop = false, gCount = 1, expandGrid = true, nextGCount = 0, arrG = tmpArrG1, nextArrG = tmpArrG2, maxRange = 0, expandRange = true, gridId: number, gridPointsFinished = false;
+    const expandedArrG = tmpArrG3, sqMaxRadius = maxRadius * maxRadius;
+    arrG.length = 0;
+    expandedArrG.length = 0;
+    tmpSetG.clear();
+    tmpHeapG.clear();
+    Vec3.set(tmpVec, x, y, z);
+    if (!Box3D.containsVec3(box, tmpVec)) {
+        // intersect ray pointing to box center
+        Box3D.nearestIntersectionWithRay(tmpVec, box, tmpVec, Vec3.normalize(tmpDirVec, Vec3.sub(tmpDirVec, center, tmpVec)));
+        gX = Math.max(0, Math.min(sX - 1, Math.floor((tmpVec[0] - min[0]) / delta[0])));
+        gY = Math.max(0, Math.min(sY - 1, Math.floor((tmpVec[1] - min[1]) / delta[1])));
+        gZ = Math.max(0, Math.min(sZ - 1, Math.floor((tmpVec[2] - min[2]) / delta[2])));
+    } else {
+        gX = Math.floor((x - min[0]) / delta[0]);
+        gY = Math.floor((y - min[1]) / delta[1]);
+        gZ = Math.floor((z - min[2]) / delta[2]);
+    }
+    const dX = maxRadius !== 0 ? Math.max(1, Math.min(sX - 1, Math.ceil(maxRadius / delta[0]))) : 1;
+    const dY = maxRadius !== 0 ? Math.max(1, Math.min(sY - 1, Math.ceil(maxRadius / delta[1]))) : 1;
+    const dZ = maxRadius !== 0 ? Math.max(1, Math.min(sZ - 1, Math.ceil(maxRadius / delta[2]))) : 1;
+    arrG.push(gX, gY, gZ, (((gX * sY) + gY) * sZ) + gZ);
+    while (result.count < indicesCount) {
+        const arrGLen = gCount * 4;
+        for (let ig = 0; ig < arrGLen; ig += 4) {
+            gridId = arrG[ig + 3];
+            if (!tmpSetG.has(gridId)) {
+                tmpSetG.add(gridId);
+                gridPointsFinished = tmpSetG.size >= grid.length;
+                const bucketIdx = grid[gridId];
+                if (bucketIdx !== 0) {
+                    const _maxRange = maxRange;
+                    const ki = bucketIdx - 1;
+                    const offset = bucketOffset[ki];
+                    const count = bucketCounts[ki];
+                    const end = offset + count;
+                    for (let i = offset; i < end; i++) {
+                        const bIdx = bucketArray[i];
+                        const idx = OrderedSet.getAt(indices, bIdx);
+                        const dx = px[idx] - x;
+                        const dy = py[idx] - y;
+                        const dz = pz[idx] - z;
+                        let distSq = dx * dx + dy * dy + dz * dz;
+                        if (maxRadius !== 0) {
+                            const r = radius![idx];
+                            distSq -= r * r;
+                        }
+                        if (expandRange && distSq > maxRange) {
+                            maxRange = distSq;
+                        }
+                        tmpHeapG.insert(distSq, bIdx);
+                    }
+                    if (_maxRange < maxRange) expandRange = false;
+                }
+            }
+        }
+        // find next grid points
+        nextArrG.length = 0;
+        nextGCount = 0;
+        tmpSetG2.clear();
+        for (let ig = 0; ig < arrGLen; ig += 4) {
+            gX = arrG[ig];
+            gY = arrG[ig + 1];
+            gZ = arrG[ig + 2];
+            // fill grid points array with valid adiacent positions
+            for (let ix = -dX; ix <= dX; ix++) {
+                const xPos = gX + ix;
+                if (xPos < 0 || xPos >= sX) continue;
+                for (let iy = -dY; iy <= dY; iy++) {
+                    const yPos = gY + iy;
+                    if (yPos < 0 || yPos >= sY) continue;
+                    for (let iz = -dZ; iz <= dZ; iz++) {
+                        const zPos = gZ + iz;
+                        if (zPos < 0 || zPos >= sZ) continue;
+                        gridId = (((xPos * sY) + yPos) * sZ) + zPos;
+                        if (tmpSetG2.has(gridId)) continue; // already scanned
+                        tmpSetG2.add(gridId);
+                        if (tmpSetG.has(gridId)) continue; // already visited
+                        if (!expandGrid) {
+                            const xP = min[0] + xPos * delta[0] - x;
+                            const yP = min[1] + yPos * delta[1] - y;
+                            const zP = min[2] + zPos * delta[2] - z;
+                            const distSqG = (xP * xP) + (yP * yP) + (zP * zP) - sqMaxRadius; // is sqMaxRadius necessary?
+                            if (distSqG > maxRange) {
+                                expandedArrG.push(xPos, yPos, zPos, gridId);
+                                continue;
+                            }
+                        }
+                        nextArrG.push(xPos, yPos, zPos, gridId);
+                        nextGCount++;
+                    }
+                }
+            }
+        }
+        expandGrid = false;
+        if (nextGCount === 0) {
+            if (k === 1) {
+                const node = tmpHeapG.findMinimum();
+                if (node) {
+                    const { key: squaredDistance, value: index } = node!;
+                    // const squaredDistance = node!.key, index = node!.value;
+                    Result.add(result, index as number, squaredDistance as number);
+                    return true;
+                }
+            } else {
+                while (!tmpHeapG.isEmpty() && (gridPointsFinished || tmpHeapG.findMinimum()!.key as number <= maxRange) && result.count < k) {
+                    const node = tmpHeapG.extractMinimum();
+                    const squaredDistance = node!.key, index = node!.value;
+                    Result.add(result, index as number, squaredDistance as number);
+                    if (stopIf && !stop) {
+                        stop = stopIf(index, squaredDistance);
+                    }
+                }
+            }
+            if (result.count >= k || stop || result.count >= indicesCount) return result.count > 0;
+            expandGrid = true;
+            expandRange = true;
+            if (expandedArrG.length > 0) {
+                for (let i = 0, l = expandedArrG.length; i < l; i++) {
+                    arrG.push(expandedArrG[i]);
+                }
+                expandedArrG.length = 0;
+                gCount = arrG.length;
+            }
+        } else {
+            const tmp = arrG;
+            arrG = nextArrG;
+            nextArrG = tmp;
+            gCount = nextGCount;
+        }
+    }
+    return result.count > 0;
+}
diff --git a/src/mol-math/geometry/primitives/box3d.ts b/src/mol-math/geometry/primitives/box3d.ts
index d701ae18085f5b45feb9f29cb7cdd5682e9104f9..0176be2aed2a120beb44c398d851539a3d1c1f43 100644
--- a/src/mol-math/geometry/primitives/box3d.ts
+++ b/src/mol-math/geometry/primitives/box3d.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -124,12 +124,62 @@ namespace Box3D {
     }
 
     export function containsVec3(box: Box3D, v: Vec3) {
-        return (
+        return !(
             v[0] < box.min[0] || v[0] > box.max[0] ||
             v[1] < box.min[1] || v[1] > box.max[1] ||
             v[2] < box.min[2] || v[2] > box.max[2]
-        ) ? false : true;
+        );
+    }
+
+    export function overlaps(a: Box3D, b: Box3D) {
+        return !(
+            a.max[0] < b.min[0] || a.min[0] > b.max[0] ||
+            a.max[1] < b.min[1] || a.min[1] > b.max[1] ||
+            a.max[2] < b.min[2] || a.min[2] > b.max[2]
+        );
+    }
+
+    // const tmpTransformV = Vec3();
+    export function nearestIntersectionWithRay(out: Vec3, box: Box3D, origin: Vec3, dir: Vec3): Vec3 {
+        const [minX, minY, minZ] = box.min;
+        const [maxX, maxY, maxZ] = box.max;
+        const [x, y, z] = origin;
+        const invDirX = 1.0 / dir[0];
+        const invDirY = 1.0 / dir[1];
+        const invDirZ = 1.0 / dir[2];
+        let tmin, tmax, tymin, tymax, tzmin, tzmax;
+        if (invDirX >= 0) {
+            tmin = (minX - x) * invDirX;
+            tmax = (maxX - x) * invDirX;
+        } else {
+            tmin = (maxX - x) * invDirX;
+            tmax = (minX - x) * invDirX;
+        }
+        if (invDirY >= 0) {
+            tymin = (minY - y) * invDirY;
+            tymax = (maxY - y) * invDirY;
+        } else {
+            tymin = (maxY - y) * invDirY;
+            tymax = (minY - y) * invDirY;
+        }
+        if (invDirZ >= 0) {
+            tzmin = (minZ - z) * invDirZ;
+            tzmax = (maxZ - z) * invDirZ;
+        } else {
+            tzmin = (maxZ - z) * invDirZ;
+            tzmax = (minZ - z) * invDirZ;
+        }
+        if (tymin > tmin)
+            tmin = tymin;
+        if (tymax < tmax)
+            tmax = tymax;
+        if (tzmin > tmin)
+            tmin = tzmin;
+        if (tzmax < tmax)
+            tmax = tzmax;
+        Vec3.scale(out, dir, tmin);
+        return Vec3.set(out, out[0] + x, out[1] + y, out[2] + z);
     }
 }
 
-export { Box3D };
\ No newline at end of file
+export { Box3D };
diff --git a/src/mol-math/geometry/primitives/sphere3d.ts b/src/mol-math/geometry/primitives/sphere3d.ts
index 6f09cce9503363b2c5da245f0198bf8a478c63f7..69953f360f467ce8a3ae6200b4b2517c135a7fe4 100644
--- a/src/mol-math/geometry/primitives/sphere3d.ts
+++ b/src/mol-math/geometry/primitives/sphere3d.ts
@@ -277,6 +277,12 @@ namespace Sphere3D {
     export function distance(a: Sphere3D, b: Sphere3D) {
         return Vec3.distance(a.center, b.center) - a.radius + b.radius;
     }
+
+    /** Get the distance of v from sphere. If negative, v is inside sphere */
+    export function distanceToVec(sphere: Sphere3D, v: Vec3): number {
+        const { center, radius } = sphere;
+        return Vec3.distance(v, center) - radius;
+    }
 }
 
-export { Sphere3D };
\ No newline at end of file
+export { Sphere3D };
diff --git a/src/mol-math/linear-algebra/3d/vec3.ts b/src/mol-math/linear-algebra/3d/vec3.ts
index 5c5e39f21ed821b16da7cbd614dcf38943468358..d55b4ee8a79c6abccef76f75969fffe2813ffe83 100644
--- a/src/mol-math/linear-algebra/3d/vec3.ts
+++ b/src/mol-math/linear-algebra/3d/vec3.ts
@@ -35,7 +35,7 @@ function Vec3() {
 
 namespace Vec3 {
     export function zero(): Vec3 {
-        const out = [0.1, 0.0, 0.0];
+        const out = [0.1, 0.0, 0.0]; // ensure backing array of type double
         out[0] = 0;
         return out as any;
     }
diff --git a/src/mol-math/linear-algebra/matrix/principal-axes.ts b/src/mol-math/linear-algebra/matrix/principal-axes.ts
index 2f28473a06a85a72cbf6136c789c007a758a6126..1150702a448ad9341c3f844c9811f13b81995a1a 100644
--- a/src/mol-math/linear-algebra/matrix/principal-axes.ts
+++ b/src/mol-math/linear-algebra/matrix/principal-axes.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,6 +9,7 @@ import { Vec3 } from '../3d/vec3';
 import { svd } from './svd';
 import { NumberArray } from '../../../mol-util/type-helpers';
 import { Axes3D } from '../../geometry';
+import { EPSILON } from '../3d/common';
 
 export { PrincipalAxes };
 
@@ -58,10 +59,15 @@ namespace PrincipalAxes {
         return Axes3D.create(origin, dirA, dirB, dirC);
     }
 
+    export function calculateNormalizedAxes(momentsAxes: Axes3D): Axes3D {
+        const a = Axes3D.clone(momentsAxes);
+        if (Vec3.magnitude(a.dirC) < EPSILON) {
+            Vec3.cross(a.dirC, a.dirA, a.dirB);
+        }
+        return Axes3D.normalize(a, a);
+    }
+
     const tmpBoxVec = Vec3();
-    const tmpBoxVecA = Vec3();
-    const tmpBoxVecB = Vec3();
-    const tmpBoxVecC = Vec3();
     /**
      * Get the scale/length for each dimension for a box around the axes
      * to enclose the given positions
@@ -82,13 +88,11 @@ namespace PrincipalAxes {
         const t = Vec3();
 
         const center = momentsAxes.origin;
-        const normVecA = Vec3.normalize(tmpBoxVecA, momentsAxes.dirA);
-        const normVecB = Vec3.normalize(tmpBoxVecB, momentsAxes.dirB);
-        const normVecC = Vec3.normalize(tmpBoxVecC, momentsAxes.dirC);
+        const a = calculateNormalizedAxes(momentsAxes);
 
         for (let i = 0, il = positions.length; i < il; i += 3) {
-            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), normVecA, center);
-            const dp1 = Vec3.dot(normVecA, Vec3.normalize(t, Vec3.sub(t, p, center)));
+            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), a.dirA, center);
+            const dp1 = Vec3.dot(a.dirA, Vec3.normalize(t, Vec3.sub(t, p, center)));
             const dt1 = Vec3.distance(p, center);
             if (dp1 > 0) {
                 if (dt1 > d1a) d1a = dt1;
@@ -96,8 +100,8 @@ namespace PrincipalAxes {
                 if (dt1 > d1b) d1b = dt1;
             }
 
-            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), normVecB, center);
-            const dp2 = Vec3.dot(normVecB, Vec3.normalize(t, Vec3.sub(t, p, center)));
+            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), a.dirB, center);
+            const dp2 = Vec3.dot(a.dirB, Vec3.normalize(t, Vec3.sub(t, p, center)));
             const dt2 = Vec3.distance(p, center);
             if (dp2 > 0) {
                 if (dt2 > d2a) d2a = dt2;
@@ -105,8 +109,8 @@ namespace PrincipalAxes {
                 if (dt2 > d2b) d2b = dt2;
             }
 
-            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), normVecC, center);
-            const dp3 = Vec3.dot(normVecC, Vec3.normalize(t, Vec3.sub(t, p, center)));
+            Vec3.projectPointOnVector(p, Vec3.fromArray(p, positions, i), a.dirC, center);
+            const dp3 = Vec3.dot(a.dirC, Vec3.normalize(t, Vec3.sub(t, p, center)));
             const dt3 = Vec3.distance(p, center);
             if (dp3 > 0) {
                 if (dt3 > d3a) d3a = dt3;
@@ -115,16 +119,16 @@ namespace PrincipalAxes {
             }
         }
 
-        const dirA = Vec3.setMagnitude(Vec3(), normVecA, (d1a + d1b) / 2);
-        const dirB = Vec3.setMagnitude(Vec3(), normVecB, (d2a + d2b) / 2);
-        const dirC = Vec3.setMagnitude(Vec3(), normVecC, (d3a + d3b) / 2);
+        const dirA = Vec3.setMagnitude(Vec3(), a.dirA, (d1a + d1b) / 2);
+        const dirB = Vec3.setMagnitude(Vec3(), a.dirB, (d2a + d2b) / 2);
+        const dirC = Vec3.setMagnitude(Vec3(), a.dirC, (d3a + d3b) / 2);
 
         const origin = Vec3();
         const addCornerHelper = function (d1: number, d2: number, d3: number) {
             Vec3.copy(tmpBoxVec, center);
-            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, normVecA, d1);
-            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, normVecB, d2);
-            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, normVecC, d3);
+            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, a.dirA, d1);
+            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, a.dirB, d2);
+            Vec3.scaleAndAdd(tmpBoxVec, tmpBoxVec, a.dirC, d3);
             Vec3.add(origin, origin, tmpBoxVec);
         };
         addCornerHelper(d1a, d2a, d3a);
diff --git a/src/mol-model-formats/shape/ply.ts b/src/mol-model-formats/shape/ply.ts
index d94a8047f4b8777bcbca1fadb303206cd0da0863..3d488a8889a009b5e7286f41289669afe786931e 100644
--- a/src/mol-model-formats/shape/ply.ts
+++ b/src/mol-model-formats/shape/ply.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Schäfer, Marco <marco.schaefer@uni-tuebingen.de>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -19,6 +19,7 @@ import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ColorNames } from '../../mol-util/color/names';
 import { deepClone } from '../../mol-util/object';
 import { stringToWords } from '../../mol-util/string';
+import { ValueCell } from '../../mol-util/value-cell';
 
 // TODO support 'edge' element, see https://www.mathworks.com/help/vision/ug/the-ply-format.html
 // TODO support missing face element
@@ -170,6 +171,9 @@ async function getMesh(ctx: RuntimeContext, vertex: PlyTable, face: PlyList, gro
     const m = MeshBuilder.getMesh(builderState);
     if (!hasNormals) Mesh.computeNormals(m);
 
+    // TODO: check if needed
+    ValueCell.updateIfChanged(m.varyingGroup, true);
+
     return m;
 }
 
diff --git a/src/mol-model-formats/structure/common/component.ts b/src/mol-model-formats/structure/common/component.ts
index 948a01fcb7df8a3b8942d5798a106a5c13588416..770a095cac67716d4bb64b82157b2a6a4a5af33b 100644
--- a/src/mol-model-formats/structure/common/component.ts
+++ b/src/mol-model-formats/structure/common/component.ts
@@ -32,6 +32,7 @@ const DnaAtomIdsList = [
 /** Used to reduce false positives for atom name-based type guessing */
 const NonPolymerNames = new Set([
     'FMN', 'NCN', 'FNS', 'FMA', 'ATP', 'ADP', 'AMP', 'GTP', 'GDP', 'GMP', // Mononucleotides
+    'LIG'
 ]);
 
 const StandardComponents = (function () {
diff --git a/src/mol-model-formats/structure/mol.ts b/src/mol-model-formats/structure/mol.ts
index 942f24a597cd0c8bbb18c564d571536e9310f49f..f32849f738b1bb31f4a9aca641bbe2a55adf1e91 100644
--- a/src/mol-model-formats/structure/mol.ts
+++ b/src/mol-model-formats/structure/mol.ts
@@ -80,7 +80,10 @@ export async function getMolModels(mol: MolFile, format: ModelFormat<any> | unde
         const indexA = Column.ofIntArray(Column.mapToArray(bonds.atomIdxA, x => x - 1, Int32Array));
         const indexB = Column.ofIntArray(Column.mapToArray(bonds.atomIdxB, x => x - 1, Int32Array));
         const order = Column.asArrayColumn(bonds.order, Int32Array);
-        const pairBonds = IndexPairBonds.fromData({ pairs: { indexA, indexB, order }, count: atoms.count });
+        const pairBonds = IndexPairBonds.fromData(
+            { pairs: { indexA, indexB, order }, count: atoms.count },
+            { maxDistance: Infinity }
+        );
         IndexPairBonds.Provider.set(models.representative, pairBonds);
     }
 
diff --git a/src/mol-model-formats/structure/mol2.ts b/src/mol-model-formats/structure/mol2.ts
index ac8b4e75c1119a06afa11a91d18709f888129418..19723a6fd1987e77bb382bafb2621f98e95bfbeb 100644
--- a/src/mol-model-formats/structure/mol2.ts
+++ b/src/mol-model-formats/structure/mol2.ts
@@ -113,7 +113,10 @@ async function getModels(mol2: Mol2File, ctx: RuntimeContext) {
                         return BondType.Flag.Covalent;
                 }
             }, Int8Array));
-            const pairBonds = IndexPairBonds.fromData({ pairs: { key, indexA, indexB, order, flag }, count: atoms.count });
+            const pairBonds = IndexPairBonds.fromData(
+                { pairs: { key, indexA, indexB, order, flag }, count: atoms.count },
+                { maxDistance: crysin ? -1 : Infinity }
+            );
 
             const first = _models.representative;
             IndexPairBonds.Provider.set(first, pairBonds);
diff --git a/src/mol-model-formats/structure/pdb/atom-site.ts b/src/mol-model-formats/structure/pdb/atom-site.ts
index 7de7aeed37eb9c36cd14b717f05b7b6b02f6b3f9..c8b1c996077de39d522b6916f161b69de7fcb27b 100644
--- a/src/mol-model-formats/structure/pdb/atom-site.ts
+++ b/src/mol-model-formats/structure/pdb/atom-site.ts
@@ -39,7 +39,7 @@ export function getAtomSiteTemplate(data: string, count: number) {
     };
 }
 
-export function getAtomSite(sites: AtomSiteTemplate, hasTer: boolean): { [K in keyof mmCIF_Schema['atom_site'] | 'partial_charge']?: CifField } {
+export function getAtomSite(sites: AtomSiteTemplate, terIndices: Set<number>): { [K in keyof mmCIF_Schema['atom_site'] | 'partial_charge']?: CifField } {
     const pdbx_PDB_model_num = CifField.ofStrings(sites.pdbx_PDB_model_num);
     const auth_asym_id = CifField.ofTokens(sites.auth_asym_id);
     const auth_seq_id = CifField.ofTokens(sites.auth_seq_id);
@@ -67,21 +67,17 @@ export function getAtomSite(sites: AtomSiteTemplate, hasTer: boolean): { [K in k
         const seqId = auth_seq_id.int(i);
         let atomId = auth_atom_id.str(i);
 
-        let asymIdChanged = false;
-
         if (modelNum !== currModelNum) {
             asymIdCounts.clear();
             atomIdCounts.clear();
             currModelNum = modelNum;
             currAsymId = asymId;
             currSeqId = seqId;
-            asymIdChanged = true;
             currLabelAsymId = asymId;
         } else if (currAsymId !== asymId) {
             atomIdCounts.clear();
             currAsymId = asymId;
             currSeqId = seqId;
-            asymIdChanged = true;
             currLabelAsymId = asymId;
         } else if (currSeqId !== seqId) {
             atomIdCounts.clear();
@@ -91,7 +87,7 @@ export function getAtomSite(sites: AtomSiteTemplate, hasTer: boolean): { [K in k
         if (asymIdCounts.has(asymId)) {
             // only change the chains name if there are TER records
             // otherwise assume repeated chain name use is from interleaved chains
-            if (hasTer && asymIdChanged) {
+            if (terIndices.has(i)) {
                 const asymIdCount = asymIdCounts.get(asymId)! + 1;
                 asymIdCounts.set(asymId, asymIdCount);
                 currLabelAsymId = `${asymId}_${asymIdCount}`;
diff --git a/src/mol-model-formats/structure/pdb/to-cif.ts b/src/mol-model-formats/structure/pdb/to-cif.ts
index 21a2a68f0593c6aeecf3d549600b0836290d167d..792d5a136d2c45388eba6ca8612803c890c7d32c 100644
--- a/src/mol-model-formats/structure/pdb/to-cif.ts
+++ b/src/mol-model-formats/structure/pdb/to-cif.ts
@@ -51,7 +51,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
 
     let modelNum = 0, modelStr = '';
     let conectRange: [number, number] | undefined = undefined;
-    let hasTer = false;
+    const terIndices = new Set<number>();
 
     for (let i = 0, _i = lines.count; i < _i; i++) {
         let s = indices[2 * i], e = indices[2 * i + 1];
@@ -164,7 +164,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
                 break;
             case 'T':
                 if (substringStartsWith(data, s, e, 'TER')) {
-                    hasTer = true;
+                    terIndices.add(atomSite.index);
                 }
         }
     }
@@ -183,7 +183,7 @@ export async function pdbToMmCif(pdb: PdbFile): Promise<CifFrame> {
         atomSite.label_entity_id[i] = entityBuilder.getEntityId(compId, moleculeType, asymIds.value(i));
     }
 
-    const atom_site = getAtomSite(atomSite, hasTer);
+    const atom_site = getAtomSite(atomSite, terIndices);
     if (!isPdbqt) delete atom_site.partial_charge;
 
     if (conectRange) {
diff --git a/src/mol-model-formats/structure/property/bonds/chem_comp.ts b/src/mol-model-formats/structure/property/bonds/chem_comp.ts
index b3f64adcff90adfd48410aa33035248c7f740ada..6ba2bc374891a16fff526ee3e8b7b96a3db37b70 100644
--- a/src/mol-model-formats/structure/property/bonds/chem_comp.ts
+++ b/src/mol-model-formats/structure/property/bonds/chem_comp.ts
@@ -65,7 +65,7 @@ export namespace ComponentBond {
             return e;
         }
 
-        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount } = data;
+        const { comp_id, atom_id_1, atom_id_2, value_order, pdbx_aromatic_flag, _rowCount, pdbx_ordinal } = data;
 
         let entry = addEntry(comp_id.value(0)!);
         for (let i = 0; i < _rowCount; i++) {
@@ -74,6 +74,7 @@ export namespace ComponentBond {
             const nameB = atom_id_2.value(i)!;
             const order = value_order.value(i)!;
             const aromatic = pdbx_aromatic_flag.value(i) === 'y';
+            const key = pdbx_ordinal.value(i);
 
             if (entry.id !== id) {
                 entry = addEntry(id);
@@ -89,29 +90,29 @@ export namespace ComponentBond {
                 case 'quad': ord = 4; break;
             }
 
-            entry.add(nameA, nameB, ord, flags);
+            entry.add(nameA, nameB, ord, flags, key);
         }
 
         return entries;
     }
 
     export class Entry {
-        readonly map: Map<string, Map<string, { order: number, flags: number }>> = new Map();
+        readonly map: Map<string, Map<string, { order: number, flags: number, key: number }>> = new Map();
 
-        add(a: string, b: string, order: number, flags: number, swap = true) {
+        add(a: string, b: string, order: number, flags: number, key: number, swap = true) {
             const e = this.map.get(a);
             if (e !== void 0) {
                 const f = e.get(b);
                 if (f === void 0) {
-                    e.set(b, { order, flags });
+                    e.set(b, { order, flags, key });
                 }
             } else {
-                const map = new Map<string, { order: number, flags: number }>();
-                map.set(b, { order, flags });
+                const map = new Map<string, { order: number, flags: number, key: number }>();
+                map.set(b, { order, flags, key });
                 this.map.set(a, map);
             }
 
-            if (swap) this.add(b, a, order, flags, false);
+            if (swap) this.add(b, a, order, flags, key, false);
         }
 
         constructor(public readonly id: string) { }
diff --git a/src/mol-model-formats/structure/property/bonds/struct_conn.ts b/src/mol-model-formats/structure/property/bonds/struct_conn.ts
index d8b2ca61e29bccaecdd16a293ef519eceb144cb6..f5f38998ec4da81174f5787b5e84719dc4c52fbc 100644
--- a/src/mol-model-formats/structure/property/bonds/struct_conn.ts
+++ b/src/mol-model-formats/structure/property/bonds/struct_conn.ts
@@ -111,24 +111,28 @@ export namespace StructConn {
             symmetry: struct_conn.ptnr2_symmetry
         };
 
+        const entityIds = Array.from(model.entities.data.id.toArray());
         const _p = (row: number, ps: typeof p1) => {
             if (ps.label_asym_id.valueKind(row) !== Column.ValueKind.Present) return void 0;
             const asymId = ps.label_asym_id.value(row);
-            const entityIndex = model.atomicHierarchy.index.findEntity(asymId);
-            if (entityIndex < 0) return void 0;
-            const residueIndex = model.atomicHierarchy.index.findResidue(
-                model.entities.data.id.value(entityIndex),
-                asymId,
-                ps.auth_seq_id.value(row),
-                ps.ins_code.value(row)
-            );
-            if (residueIndex < 0) return void 0;
             const atomName = ps.label_atom_id.value(row);
             // turns out "mismat" records might not have atom name value
-            if (!atomName) return void 0;
-            const atomIndex = model.atomicHierarchy.index.findAtomOnResidue(residueIndex, atomName, ps.label_alt_id.value(row));
-            if (atomIndex < 0) return void 0;
-            return { residueIndex, atomIndex, symmetry: ps.symmetry.value(row) };
+            if (!atomName) return undefined;
+
+            const altId = ps.label_alt_id.value(row);
+            for (const eId of entityIds) {
+                const residueIndex = model.atomicHierarchy.index.findResidue(
+                    eId,
+                    asymId,
+                    ps.auth_seq_id.value(row),
+                    ps.ins_code.value(row)
+                );
+                if (residueIndex < 0) continue;
+                const atomIndex = model.atomicHierarchy.index.findAtomOnResidue(residueIndex, atomName, altId);
+                if (atomIndex < 0) continue;
+                return { residueIndex, atomIndex, symmetry: ps.symmetry.value(row) };
+            }
+            return void 0;
         };
 
         const entries: StructConn.Entry[] = [];
diff --git a/src/mol-model-props/common/custom-element-property.ts b/src/mol-model-props/common/custom-element-property.ts
index 8f60da9d85a008d47d0120c60fdce7ceff08b03b..47580e1985a8d146db25eb5c2111234d59c6799e 100644
--- a/src/mol-model-props/common/custom-element-property.ts
+++ b/src/mol-model-props/common/custom-element-property.ts
@@ -106,7 +106,7 @@ namespace CustomElementProperty {
             factory: Coloring,
             getParams: () => ({}),
             defaultValues: {},
-            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && !!modelProperty.get(ctx.structure.models[0]).value,
+            isApplicable: (ctx: ThemeDataContext) => !!ctx.structure,
             ensureCustomProperties: {
                 attach: (ctx: CustomProperty.Context, data: ThemeDataContext) => data.structure ? modelProperty.attach(ctx, data.structure.models[0], void 0, true) : Promise.resolve(),
                 detach: (data: ThemeDataContext) => data.structure && data.structure.models[0].customProperties.reference(modelProperty.descriptor, false)
diff --git a/src/mol-model-props/common/custom-model-property.ts b/src/mol-model-props/common/custom-model-property.ts
index 5d9b001ae9e6a15550e8177d057475656f0c6485..81004faf0939784f7e528319d2c3d17c4ac29889 100644
--- a/src/mol-model-props/common/custom-model-property.ts
+++ b/src/mol-model-props/common/custom-model-property.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -71,12 +71,12 @@ namespace CustomModelProperty {
             },
             ref: (data: Model, add: boolean) => data.customProperties.reference(builder.descriptor, add),
             get: (data: Model) => get(data)?.data,
-            set: (data: Model, props: Partial<PD.Values<Params>> = {}) => {
+            set: (data: Model, props: Partial<PD.Values<Params>> = {}, value?: Value) => {
                 const property = get(data);
                 const p = PD.merge(builder.defaultParams, property.props, props);
                 if (!PD.areEqual(builder.defaultParams, property.props, p)) {
                     // this invalidates property.value
-                    set(data, p, undefined);
+                    set(data, p, value);
                     // dispose of assets
                     data.customProperties.assets(builder.descriptor);
                 }
@@ -96,7 +96,7 @@ namespace CustomModelProperty {
             getParams: () => ({ value: PD.Value(defaultValue, { isHidden: true }) }),
             isApplicable: () => true,
             obtain: async (ctx: CustomProperty.Context, data: Model, props: Partial<PD.Values<typeof defaultParams>>) => {
-                return { value: props.value ?? defaultValue };
+                return { ...PD.getDefaultValues(defaultParams), ...props };
             }
         });
     }
diff --git a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
index d07ee17b564f5f7cacac9fc97312946db51eecdf..9a0d9164212b5f844665d350b75a970d7621513c 100644
--- a/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-inter-unit-cylinder.ts
@@ -22,6 +22,8 @@ import { LocationIterator } from '../../../mol-geo/util/location-iterator';
 import { InteractionFlag } from '../interactions/common';
 import { Unit } from '../../../mol-model/structure/structure';
 import { Sphere3D } from '../../../mol-math/geometry';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: Structure, theme: Theme, props: PD.Values<InteractionsInterUnitParams>, mesh?: Mesh) {
     if (!structure.hasAtomic) return Mesh.createEmpty(mesh);
@@ -31,7 +33,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
     const { contacts, unitsFeatures } = interactions;
 
     const { edgeCount, edges } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -70,14 +72,48 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 
             if (child) {
                 const b = edges[edgeIndex];
-                const childUnitA = child.unitMap.get(b.unitA);
-                if (!childUnitA) return true;
-
-                const unitA = structure.unitMap.get(b.unitA);
-                const { offsets, members } = unitsFeatures.get(b.unitA);
-                for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
-                    const eA = unitA.elements[members[i]];
-                    if (!SortedArray.has(childUnitA.elements, eA)) return true;
+
+                if (parentDisplay === 'stub') {
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) return true;
+
+                    const unitA = structure.unitMap.get(b.unitA);
+                    const { offsets, members } = unitsFeatures.get(b.unitA);
+                    for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                        const eA = unitA.elements[members[i]];
+                        if (!SortedArray.has(childUnitA.elements, eA)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const childUnitA = child.unitMap.get(b.unitA);
+                    if (!childUnitA) {
+                        flagA = true;
+                    } else {
+                        const unitA = structure.unitMap.get(b.unitA);
+                        const { offsets, members } = unitsFeatures.get(b.unitA);
+                        for (let i = offsets[b.indexA], il = offsets[b.indexA + 1]; i < il; ++i) {
+                            const eA = unitA.elements[members[i]];
+                            if (!SortedArray.has(childUnitA.elements, eA)) flagA = true;
+                        }
+                    }
+
+                    const childUnitB = child.unitMap.get(b.unitB);
+                    if (!childUnitB) {
+                        flagB = true;
+                    } else {
+                        const unitB = structure.unitMap.get(b.unitB);
+                        const { offsets, members } = unitsFeatures.get(b.unitB);
+                        for (let i = offsets[b.indexB], il = offsets[b.indexB + 1]; i < il; ++i) {
+                            const eB = unitB.elements[members[i]];
+                            if (!SortedArray.has(childUnitB.elements, eB)) flagB = true;
+                        }
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
             }
 
@@ -101,10 +137,7 @@ function createInterUnitInteractionCylinderMesh(ctx: VisualContext, structure: S
 export const InteractionsInterUnitParams = {
     ...ComplexMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 export type InteractionsInterUnitParams = typeof InteractionsInterUnitParams
 
@@ -121,7 +154,8 @@ export function InteractionsInterUnitVisual(materialId: number): ComplexVisual<I
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
 
             const interactionsHash = InteractionsProvider.get(newStructure).version;
diff --git a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
index de3675163db17e5eb355e8c6a9f9c6e9ffee9863..e0289f7b75aeffbf37ba01dd2ff02a6194b75172 100644
--- a/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
+++ b/src/mol-model-props/computed/representations/interactions-intra-unit-cylinder.ts
@@ -22,6 +22,8 @@ import { Interactions } from '../interactions/interactions';
 import { InteractionFlag } from '../interactions/common';
 import { Sphere3D } from '../../../mol-math/geometry';
 import { StructureGroup } from '../../../mol-repr/structure/visual/util/common';
+import { assertUnreachable } from '../../../mol-util/type-helpers';
+import { InteractionsSharedParams } from './shared';
 
 async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit: Unit, structure: Structure, theme: Theme, props: PD.Values<InteractionsIntraUnitParams>, mesh?: Mesh) {
     if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh);
@@ -38,7 +40,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 
     const { x, y, z, members, offsets } = features;
     const { edgeCount, a, b, edgeProps: { flag } } = contacts;
-    const { sizeFactor } = props;
+    const { sizeFactor, parentDisplay } = props;
 
     if (!edgeCount) return Mesh.createEmpty(mesh);
 
@@ -60,10 +62,31 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
             if (flag[edgeIndex] === InteractionFlag.Filtered) return true;
 
             if (childUnit) {
-                const f = a[edgeIndex];
-                for (let i = offsets[f], jl = offsets[f + 1]; i < jl; ++i) {
-                    const e = unit.elements[members[offsets[i]]];
-                    if (!SortedArray.has(childUnit.elements, e)) return true;
+                if (parentDisplay === 'stub') {
+                    const f = a[edgeIndex];
+                    for (let i = offsets[f], il = offsets[f + 1]; i < il; ++i) {
+                        const e = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, e)) return true;
+                    }
+                } else if (parentDisplay === 'full' || parentDisplay === 'between') {
+                    let flagA = false;
+                    let flagB = false;
+
+                    const fA = a[edgeIndex];
+                    for (let i = offsets[fA], il = offsets[fA + 1]; i < il; ++i) {
+                        const eA = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eA)) flagA = true;
+                    }
+
+                    const fB = b[edgeIndex];
+                    for (let i = offsets[fB], il = offsets[fB + 1]; i < il; ++i) {
+                        const eB = unit.elements[members[offsets[i]]];
+                        if (!SortedArray.has(childUnit.elements, eB)) flagB = true;
+                    }
+
+                    return parentDisplay === 'full' ? flagA && flagB : flagA === flagB;
+                } else {
+                    assertUnreachable(parentDisplay);
                 }
             }
 
@@ -86,10 +109,7 @@ async function createIntraUnitInteractionsCylinderMesh(ctx: VisualContext, unit:
 export const InteractionsIntraUnitParams = {
     ...UnitsMeshParams,
     ...LinkCylinderParams,
-    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
-    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
-    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
-    includeParent: PD.Boolean(false),
+    ...InteractionsSharedParams,
 };
 export type InteractionsIntraUnitParams = typeof InteractionsIntraUnitParams
 
@@ -106,7 +126,8 @@ export function InteractionsIntraUnitVisual(materialId: number): UnitsVisual<Int
                 newProps.dashCount !== currentProps.dashCount ||
                 newProps.dashScale !== currentProps.dashScale ||
                 newProps.dashCap !== currentProps.dashCap ||
-                newProps.radialSegments !== currentProps.radialSegments
+                newProps.radialSegments !== currentProps.radialSegments ||
+                newProps.parentDisplay !== currentProps.parentDisplay
             );
 
             const interactionsHash = InteractionsProvider.get(newStructureGroup.structure).version;
diff --git a/src/mol-model-props/computed/representations/shared.ts b/src/mol-model-props/computed/representations/shared.ts
new file mode 100644
index 0000000000000000000000000000000000000000..61d70bf0745baf00cd9de2ae558a3e19116e54c1
--- /dev/null
+++ b/src/mol-model-props/computed/representations/shared.ts
@@ -0,0 +1,16 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ParamDefinition as PD } from '../../../mol-util/param-definition';
+
+export const InteractionsSharedParams = {
+    sizeFactor: PD.Numeric(0.3, { min: 0, max: 10, step: 0.01 }),
+    dashCount: PD.Numeric(6, { min: 2, max: 10, step: 2 }),
+    dashScale: PD.Numeric(0.4, { min: 0, max: 2, step: 0.1 }),
+    includeParent: PD.Boolean(false),
+    parentDisplay: PD.Select('stub', PD.arrayToOptions(['stub', 'full', 'between'] as const), { description: 'Only has an effect when "includeParent" is enabled. "Stub" shows just the child side of interactions to the parent. "Full" shows both sides of interactions to the parent. "Between" shows only interactions to the parent.' }),
+};
+export type InteractionsSharedParams = typeof InteractionsSharedParams
diff --git a/src/mol-model/loci.ts b/src/mol-model/loci.ts
index a85152bc8fcfa6dab7b9a062adb8c25ad29736fa..7118d487fac8480b7d4dad5718182f37154b97bc 100644
--- a/src/mol-model/loci.ts
+++ b/src/mol-model/loci.ts
@@ -241,7 +241,9 @@ namespace Loci {
                 ? Structure.toStructureElementLoci(loci.structure)
                 : ShapeGroup.isLoci(loci)
                     ? Shape.Loci(loci.shape)
-                    : loci;
+                    : Volume.Cell.isLoci(loci)
+                        ? Volume.Loci(loci.volume)
+                        : loci;
         },
         'elementInstances': (loci: Loci) => {
             return StructureElement.Loci.is(loci)
diff --git a/src/mol-model/structure/model/model.ts b/src/mol-model/structure/model/model.ts
index 0301ef681b83f36adeb52f6d247c8ede6f6a27ba..76e9c4077a8134dda1e41cac9ee2facd5f601eb5 100644
--- a/src/mol-model/structure/model/model.ts
+++ b/src/mol-model/structure/model/model.ts
@@ -213,6 +213,9 @@ export namespace Model {
     export type Index = number;
     export const Index = CustomModelProperty.createSimple<Index>('index', 'static');
 
+    export type MaxIndex = number;
+    export const MaxIndex = CustomModelProperty.createSimple<MaxIndex>('max_index', 'static');
+
     export function getRoot(model: Model) {
         return model.parent || model;
     }
diff --git a/src/mol-model/structure/query.ts b/src/mol-model/structure/query.ts
index f89c0259f230eb8c779775350c70c56edd141e61..afc174ba7031d3c17a85bec4b5b5373c38135042 100644
--- a/src/mol-model/structure/query.ts
+++ b/src/mol-model/structure/query.ts
@@ -12,6 +12,7 @@ import * as modifiers from './query/queries/modifiers';
 import * as filters from './query/queries/filters';
 import * as combinators from './query/queries/combinators';
 import * as internal from './query/queries/internal';
+import * as atomset from './query/queries/atom-set';
 import { Predicates as pred } from './query/predicates';
 
 export const Queries = {
@@ -20,7 +21,8 @@ export const Queries = {
     modifiers,
     combinators,
     pred,
-    internal
+    internal,
+    atomset
 };
 
-export { StructureSelection, StructureQuery };
\ No newline at end of file
+export { StructureSelection, StructureQuery };
diff --git a/src/mol-model/structure/query/context.ts b/src/mol-model/structure/query/context.ts
index 43b2538b98ba727032ab231948c203283e123f61..c51b441a77da6cfffd623877b0734df8e87fc757 100644
--- a/src/mol-model/structure/query/context.ts
+++ b/src/mol-model/structure/query/context.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Structure, StructureElement, Unit } from '../structure';
@@ -113,6 +114,7 @@ class QueryContextBondInfo<U extends Unit = Unit> {
     bIndex: StructureElement.UnitIndex = 0 as StructureElement.UnitIndex;
     type: BondType = BondType.Flag.None;
     order: number = 0;
+    key: number = -1;
 
     private testFn: QueryPredicate = defaultBondTest;
 
diff --git a/src/mol-model/structure/query/queries/atom-set.ts b/src/mol-model/structure/query/queries/atom-set.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2043bb2bcfcc81bb6143c504904ba1312cb41c8c
--- /dev/null
+++ b/src/mol-model/structure/query/queries/atom-set.ts
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma
+ * Adapted from MolQL implemtation of atom-set.ts
+ *
+ * Copyright (c) 2017 MolQL contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StructureQuery } from '../query';
+import { StructureSelection } from '../selection';
+import { getCurrentStructureProperties } from './filters';
+import { QueryContext, QueryFn } from '../context';
+
+
+export function atomCount(ctx: QueryContext) {
+    return ctx.currentStructure.elementCount;
+}
+
+
+export function countQuery(query: StructureQuery) {
+    return (ctx: QueryContext) => {
+        const sel = query(ctx);
+        return StructureSelection.structureCount(sel);
+    };
+}
+
+export function propertySet(prop: QueryFn<any>) {
+    return (ctx: QueryContext) => {
+        const set = new Set();
+        return getCurrentStructureProperties(ctx, prop, set);
+    };
+}
+
diff --git a/src/mol-model/structure/query/queries/filters.ts b/src/mol-model/structure/query/queries/filters.ts
index 128f9b4fa7d0b698d88a7f939140d5737c3f8e21..00653ccf181d8217391e28a1cbddeb04865ffa30 100644
--- a/src/mol-model/structure/query/queries/filters.ts
+++ b/src/mol-model/structure/query/queries/filters.ts
@@ -1,12 +1,13 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { SetUtils } from '../../../../mol-util/set';
 import { Unit } from '../../structure';
-import { QueryContext, QueryFn, QueryPredicate } from '../context';
+import { QueryContext, QueryFn } from '../context';
 import { StructureQuery } from '../query';
 import { StructureSelection } from '../selection';
 import { structureAreIntersecting } from '../utils/structure-set';
@@ -16,7 +17,7 @@ import { Structure } from '../../structure/structure';
 import { StructureElement } from '../../structure/element';
 import { SortedArray } from '../../../../mol-data/int';
 
-export function pick(query: StructureQuery, pred: QueryPredicate): StructureQuery {
+export function pick(query: StructureQuery, pred: QueryFn<any>): StructureQuery {
     return ctx => {
         const sel = query(ctx);
         const ret = StructureSelection.LinearBuilder(ctx.inputStructure);
@@ -50,9 +51,7 @@ export function first(query: StructureQuery): StructureQuery {
     };
 }
 
-export interface UnitTypeProperties { atomic?: QueryFn, coarse?: QueryFn }
-
-export function getCurrentStructureProperties(ctx: QueryContext, props: UnitTypeProperties, set: Set<any>) {
+export function getCurrentStructureProperties(ctx: QueryContext, props: QueryFn<any>, set: Set<any>) {
     const { units } = ctx.currentStructure;
     const l = ctx.pushCurrentElement();
 
@@ -61,9 +60,9 @@ export function getCurrentStructureProperties(ctx: QueryContext, props: UnitType
         l.unit = unit;
         const elements = unit.elements;
 
-        let fn;
-        if (Unit.isAtomic(unit)) fn = props.atomic;
-        else fn = props.coarse;
+        const fn = props;
+        //        if (Unit.isAtomic(unit)) fn = props.atomic;
+        //        else fn = props.coarse;
         if (!fn) continue;
 
         for (let j = 0, _j = elements.length; j < _j; j++) {
@@ -77,7 +76,7 @@ export function getCurrentStructureProperties(ctx: QueryContext, props: UnitType
     return set;
 }
 
-function getSelectionProperties(ctx: QueryContext, query: StructureQuery, props: UnitTypeProperties) {
+function getSelectionProperties(ctx: QueryContext, query: StructureQuery, props: QueryFn<any>) {
     const set = new Set();
 
     const sel = query(ctx);
@@ -92,7 +91,7 @@ function getSelectionProperties(ctx: QueryContext, query: StructureQuery, props:
     return set;
 }
 
-export function withSameAtomProperties(query: StructureQuery, propertySource: StructureQuery, props: UnitTypeProperties): StructureQuery {
+export function withSameAtomProperties(query: StructureQuery, propertySource: StructureQuery, props: QueryFn<any>): StructureQuery {
     return ctx => {
         const sel = query(ctx);
         const propSet = getSelectionProperties(ctx, propertySource, props);
@@ -102,7 +101,7 @@ export function withSameAtomProperties(query: StructureQuery, propertySource: St
         StructureSelection.forEach(sel, (s, i) => {
             ctx.currentStructure = s;
             const currentProps = getCurrentStructureProperties(ctx, props, new Set());
-            if (SetUtils.isSuperset(currentProps, propSet)) {
+            if (SetUtils.isSuperset(propSet, currentProps)) {
                 ret.add(s);
             }
 
@@ -248,7 +247,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
 
         const inputUnit = input.unitMap.get(unit.id) as Unit.Atomic;
 
-        const { offset, b, edgeProps: { flags, order } } = inputUnit.bonds;
+        const { offset, b, edgeProps: { flags, order, key } } = inputUnit.bonds;
         const bondedUnits = interBonds.getConnectedUnits(unit.id);
         const buCount = bondedUnits.length;
 
@@ -273,6 +272,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
                 atomicBond.bIndex = b[l] as StructureElement.UnitIndex;
                 atomicBond.type = flags[l];
                 atomicBond.order = order[l];
+                atomicBond.key = key[l];
                 if (atomicBond.test(queryCtx, true)) return true;
             }
 
@@ -295,6 +295,7 @@ function checkConnected(ctx: IsConnectedToCtx, structure: Structure) {
                     atomicBond.bIndex = bond.indexB;
                     atomicBond.type = bond.props.flag;
                     atomicBond.order = bond.props.order;
+                    atomicBond.key = bond.props.key;
                     if (atomicBond.test(queryCtx, true)) return true;
                 }
             }
@@ -342,4 +343,4 @@ export function isConnectedTo({ query, target, disjunct, invert, bondTest }: IsC
 
         return ret.getSelection();
     };
-}
\ No newline at end of file
+}
diff --git a/src/mol-model/structure/query/queries/generators.ts b/src/mol-model/structure/query/queries/generators.ts
index e963dc2a5b2eb62f408ccde5f5bbfbf000c9510e..807114c5e2ccde81788992acb01c9719c19ec92e 100644
--- a/src/mol-model/structure/query/queries/generators.ts
+++ b/src/mol-model/structure/query/queries/generators.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2017-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2022 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>
@@ -322,7 +322,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
         for (const unit of structure.units) {
             if (unit.kind !== Unit.Kind.Atomic) continue;
 
-            const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order } } = unit.bonds;
+            const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order, key } } = unit.bonds;
             atomicBond.a.unit = unit;
             atomicBond.b.unit = unit;
             for (let i = 0 as StructureElement.UnitIndex, _i = unit.elements.length; i < _i; i++) {
@@ -335,6 +335,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
                     atomicBond.b.element = unit.elements[intraBondB[lI]];
                     atomicBond.type = flags[lI];
                     atomicBond.order = order[lI];
+                    atomicBond.key = key[lI];
                     // No need to "swap test" because each bond direction will be visited eventually.
                     if (atomicBond.test(ctx, false)) {
                         const b = structure.subsetBuilder(false);
@@ -358,6 +359,7 @@ export function bondedAtomicPairs(bondTest?: QueryPredicate): StructureQuery {
             atomicBond.bIndex = bond.indexB;
             atomicBond.order = bond.props.order;
             atomicBond.type = bond.props.flag;
+            atomicBond.key = bond.props.key;
 
             // No need to "swap test" because each bond direction will be visited eventually.
             if (atomicBond.test(ctx, false)) {
diff --git a/src/mol-model/structure/query/queries/modifiers.ts b/src/mol-model/structure/query/queries/modifiers.ts
index c860695950b6b3430e8d6f8effa9843e4456b396..f10e9f7c7683243b6795c70e74c994d624678b5d 100644
--- a/src/mol-model/structure/query/queries/modifiers.ts
+++ b/src/mol-model/structure/query/queries/modifiers.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2017-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2017-2022 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>
  */
 
 import { Segmentation, SortedArray } from '../../../../mol-data/int';
@@ -370,7 +371,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
         }
 
         const inputUnitA = inputStructure.unitMap.get(unit.id) as Unit.Atomic;
-        const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order } } = inputUnitA.bonds;
+        const { offset: intraBondOffset, b: intraBondB, edgeProps: { flags, order, key } } = inputUnitA.bonds;
 
         atomicBond.setStructure(inputStructure);
 
@@ -397,6 +398,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
                 atomicBond.b.element = bElement;
                 atomicBond.type = flags[lI];
                 atomicBond.order = order[lI];
+                atomicBond.key = key[lI];
 
                 if (atomicBond.test(ctx, true)) {
                     builder.addToUnit(unit.id, bElement);
@@ -427,6 +429,7 @@ function expandConnected(ctx: QueryContext, structure: Structure) {
                     atomicBond.b.element = bElement;
                     atomicBond.type = bond.props.flag;
                     atomicBond.order = bond.props.order;
+                    atomicBond.key = bond.props.key;
 
                     if (atomicBond.test(ctx, true)) {
                         builder.addToUnit(bondedUnit.unitB, bElement);
diff --git a/src/mol-model/structure/structure/carbohydrates/compute.ts b/src/mol-model/structure/structure/carbohydrates/compute.ts
index 20949907ab2b3a92d602121b0b429571d27e049a..3211b795adbd29230d68a7304262cf0de3dbbf9f 100644
--- a/src/mol-model/structure/structure/carbohydrates/compute.ts
+++ b/src/mol-model/structure/structure/carbohydrates/compute.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -141,7 +141,7 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
         Vec3.normalize(elements[iA].geometry.direction, elements[iA].geometry.direction);
     }
 
-    const tmpV = Vec3.zero();
+    const tmpV = Vec3();
     function fixTerminalLinkDirection(iA: number, indexB: number, unitB: Unit.Atomic) {
         const pos = unitB.conformation.position, geo = elements[iA].geometry;
         Vec3.sub(geo.direction, pos(unitB.elements[indexB], tmpV), geo.center);
@@ -189,9 +189,10 @@ export function computeCarbohydrates(structure: Structure): Carbohydrates {
                     const anomericCarbon = getAnomericCarbon(unit, ringAtoms);
 
                     const ma = PrincipalAxes.calculateMomentsAxes(getPositions(unit, ringAtoms));
-                    const center = Vec3.copy(Vec3.zero(), ma.origin);
-                    const normal = Vec3.copy(Vec3.zero(), ma.dirC);
-                    const direction = getDirection(Vec3.zero(), unit, anomericCarbon, center);
+                    const a = PrincipalAxes.calculateNormalizedAxes(ma);
+                    const center = Vec3.copy(Vec3(), a.origin);
+                    const normal = Vec3.copy(Vec3(), a.dirC);
+                    const direction = getDirection(Vec3(), unit, anomericCarbon, center);
                     Vec3.orthogonalize(direction, normal, direction);
 
                     const ringAltId = UnitRing.getAltId(unit, ringAtoms);
diff --git a/src/mol-model/structure/structure/carbohydrates/constants.ts b/src/mol-model/structure/structure/carbohydrates/constants.ts
index bc9e49e728e386db75c92dfd118ee3e875d3de9d..aeb5fd82a1c2515becfcf439750f9fb7ccb35d8d 100644
--- a/src/mol-model/structure/structure/carbohydrates/constants.ts
+++ b/src/mol-model/structure/structure/carbohydrates/constants.ts
@@ -386,16 +386,6 @@ const DefaultSaccharideCompIdMap = (function () {
                 map.set(charmm[j], saccharide);
             }
         }
-
-        const glycam = GlycamSaccharideNames[saccharide.abbr];
-        if (glycam) {
-            for (let j = 0, jl = glycam.length; j < jl; ++j) {
-                // On collision, use PDB name as default.
-                if (!map.has(glycam[j])) {
-                    map.set(glycam[j], saccharide);
-                }
-            }
-        }
     }
     SaccharideNames.forEach(name => {
         if (!map.has(name)) map.set(name, UnknownSaccharideComponent);
diff --git a/src/mol-model/structure/structure/structure.ts b/src/mol-model/structure/structure/structure.ts
index 81f84f447fa5c17191641c2d8c3b81594bcdc08d..afcec12747ba27d40354809ab07bb8cc603366b1 100644
--- a/src/mol-model/structure/structure/structure.ts
+++ b/src/mol-model/structure/structure/structure.ts
@@ -23,7 +23,7 @@ import { Carbohydrates } from './carbohydrates/data';
 import { computeCarbohydrates } from './carbohydrates/compute';
 import { Vec3, Mat4 } from '../../../mol-math/linear-algebra';
 import { idFactory } from '../../../mol-util/id-factory';
-import { GridLookup3D } from '../../../mol-math/geometry';
+import { Box3D, GridLookup3D } from '../../../mol-math/geometry';
 import { UUID } from '../../../mol-util';
 import { CustomProperties } from '../../custom-property';
 import { AtomicHierarchy } from '../model/properties/atomic';
@@ -43,6 +43,8 @@ type State = {
     lookup3d?: StructureLookup3D,
     interUnitBonds?: InterUnitBonds,
     dynamicBonds: boolean,
+    interBondsValidUnit?: (unit: Unit) => boolean,
+    interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean,
     unitSymmetryGroups?: ReadonlyArray<Unit.SymmetryGroup>,
     unitSymmetryGroupsIndexMap?: IntMap<number>,
     unitsSortedByVolume?: ReadonlyArray<Unit>;
@@ -241,6 +243,8 @@ class Structure {
             this.state.interUnitBonds = computeInterUnitBonds(this, {
                 ignoreWater: !this.dynamicBonds,
                 ignoreIon: !this.dynamicBonds,
+                validUnit: this.state.interBondsValidUnit,
+                validUnitPair: this.state.interBondsValidUnitPair,
             });
         }
         return this.state.interUnitBonds;
@@ -250,6 +254,14 @@ class Structure {
         return this.state.dynamicBonds;
     }
 
+    get interBondsValidUnit() {
+        return this.state.interBondsValidUnit;
+    }
+
+    get interBondsValidUnitPair() {
+        return this.state.interBondsValidUnitPair;
+    }
+
     get unitSymmetryGroups(): ReadonlyArray<Unit.SymmetryGroup> {
         if (this.state.unitSymmetryGroups) return this.state.unitSymmetryGroups;
         this.state.unitSymmetryGroups = StructureSymmetry.computeTransformGroups(this);
@@ -380,7 +392,12 @@ class Structure {
             parent: parent?.remapModel(m),
             label: this.label,
             interUnitBonds: dynamicBonds ? undefined : interUnitBonds,
-            dynamicBonds
+            dynamicBonds,
+            interBondsValidUnit: this.state.interBondsValidUnit,
+            interBondsValidUnitPair: this.state.interBondsValidUnitPair,
+            coordinateSystem: this.state.coordinateSystem,
+            masterModel: this.state.masterModel,
+            representativeModel: this.state.representativeModel,
         });
     }
 
@@ -428,7 +445,6 @@ class Structure {
 
 function cmpUnits(units: ArrayLike<Unit>, i: number, j: number) {
     return units[i].id - units[j].id;
-
 }
 
 function getModels(s: Structure) {
@@ -634,6 +650,8 @@ namespace Structure {
          * Also enables calculation of inter-unit bonds in water molecules.
          */
         dynamicBonds?: boolean,
+        interBondsValidUnit?: (unit: Unit) => boolean,
+        interBondsValidUnitPair?: (structure: Structure, unitA: Unit, unitB: Unit) => boolean,
         coordinateSystem?: SymmetryOperator
         label?: string
         /** Master model for structures of a protein model and multiple ligand models */
@@ -722,6 +740,12 @@ namespace Structure {
         if (props.parent) state.parent = props.parent.parent || props.parent;
         if (props.interUnitBonds) state.interUnitBonds = props.interUnitBonds;
 
+        if (props.interBondsValidUnit) state.interBondsValidUnit = props.interBondsValidUnit;
+        else if (props.parent) state.interBondsValidUnit = props.parent.interBondsValidUnit;
+
+        if (props.interBondsValidUnitPair) state.interBondsValidUnitPair = props.interBondsValidUnitPair;
+        else if (props.parent) state.interBondsValidUnitPair = props.parent.interBondsValidUnitPair;
+
         if (props.dynamicBonds) state.dynamicBonds = props.dynamicBonds;
         else if (props.parent) state.dynamicBonds = props.parent.dynamicBonds;
 
@@ -1180,7 +1204,7 @@ namespace Structure {
 
     /**
      * Iterate over all unit pairs of a structure and invokes callback for valid units
-     * and unit pairs if within a max distance.
+     * and unit pairs if their boundaries are within a max distance.
      */
     export function eachUnitPair(structure: Structure, callback: (unitA: Unit, unitB: Unit) => void, props: EachUnitPairProps) {
         const { maxRadius, validUnit, validUnitPair } = props;
@@ -1188,15 +1212,19 @@ namespace Structure {
 
         const lookup = structure.lookup3d;
         const imageCenter = Vec3();
+        const bbox = Box3D();
+        const rvec = Vec3.create(maxRadius, maxRadius, maxRadius);
 
         for (const unit of structure.units) {
             if (!validUnit(unit)) continue;
 
             const bs = unit.boundary.sphere;
+            Box3D.expand(bbox, unit.boundary.box, rvec);
             Vec3.transformMat4(imageCenter, bs.center, unit.conformation.operator.matrix);
             const closeUnits = lookup.findUnitIndices(imageCenter[0], imageCenter[1], imageCenter[2], bs.radius + maxRadius);
             for (let i = 0; i < closeUnits.count; i++) {
                 const other = structure.units[closeUnits.indices[i]];
+                if (!Box3D.overlaps(bbox, other.boundary.box)) continue;
                 if (!validUnit(other) || unit.id >= other.id || !validUnitPair(unit, other)) continue;
 
                 if (other.elements.length >= unit.elements.length) callback(unit, other);
@@ -1329,6 +1357,9 @@ namespace Structure {
     export type Index = number;
     export const Index = CustomStructureProperty.createSimple<Index>('index', 'root');
 
+    export type MaxIndex = number;
+    export const MaxIndex = CustomStructureProperty.createSimple<MaxIndex>('max_index', 'root');
+
     const PrincipalAxesProp = '__PrincipalAxes__';
     export function getPrincipalAxes(structure: Structure): PrincipalAxes {
         if (structure.currentPropertyData[PrincipalAxesProp]) return structure.currentPropertyData[PrincipalAxesProp];
diff --git a/src/mol-model/structure/structure/unit/bonds/data.ts b/src/mol-model/structure/structure/unit/bonds/data.ts
index b05df8ed1d0e27a6a5e1c4720741bcc6ae11d356..f2ab500a86fbb09a31bc4f4d72dc6ecd99cf195d 100644
--- a/src/mol-model/structure/structure/unit/bonds/data.ts
+++ b/src/mol-model/structure/structure/unit/bonds/data.ts
@@ -15,16 +15,17 @@ import { InterUnitGraph } from '../../../../../mol-math/graph/inter-unit-graph';
 type IntraUnitBonds = IntAdjacencyGraph<StructureElement.UnitIndex, {
     readonly order: ArrayLike<number>,
     readonly flags: ArrayLike<BondType.Flag>
+    readonly key: ArrayLike<number>,
 }, {
     /** can remap even with dynamicBonds on, e.g., for water molecules */
     readonly canRemap?: boolean
 }>
 
 namespace IntraUnitBonds {
-    export const Empty: IntraUnitBonds = IntAdjacencyGraph.create([], [], [], 0, { flags: [], order: [] });
+    export const Empty: IntraUnitBonds = IntAdjacencyGraph.create([], [], [], 0, { flags: [], order: [], key: [] });
 }
 
-type InterUnitEdgeProps = { readonly order: number, readonly flag: BondType.Flag }
+type InterUnitEdgeProps = { readonly order: number, readonly flag: BondType.Flag, readonly key: number }
 
 class InterUnitBonds extends InterUnitGraph<number, StructureElement.UnitIndex, InterUnitEdgeProps> {
     /** Get inter-unit bond given a bond-location */
diff --git a/src/mol-model/structure/structure/unit/bonds/inter-compute.ts b/src/mol-model/structure/structure/unit/bonds/inter-compute.ts
index 535a6d09bca596e00119faffd2cf2fcefafd9344..74f5127df14aabdc3ba3ec71e6ca40ba1c92804c 100644
--- a/src/mol-model/structure/structure/unit/bonds/inter-compute.ts
+++ b/src/mol-model/structure/structure/unit/bonds/inter-compute.ts
@@ -21,12 +21,18 @@ import { StructConn } from '../../../../../mol-model-formats/structure/property/
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { Model } from '../../../model';
 
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3distance = Vec3.distance;
+const v3set = Vec3.set;
+const v3squaredDistance = Vec3.squaredDistance;
+const v3transformMat4 = Vec3.transformMat4;
+
 const tmpDistVecA = Vec3();
 const tmpDistVecB = Vec3();
 function getDistance(unitA: Unit.Atomic, indexA: ElementIndex, unitB: Unit.Atomic, indexB: ElementIndex) {
     unitA.conformation.position(indexA, tmpDistVecA);
     unitB.conformation.position(indexB, tmpDistVecB);
-    return Vec3.distance(tmpDistVecA, tmpDistVecB);
+    return v3distance(tmpDistVecA, tmpDistVecB);
 }
 
 const _imageTransform = Mat4();
@@ -68,22 +74,22 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI = atomsA[_aI];
-        Vec3.set(_imageA, xA[aI], yA[aI], zA[aI]);
-        if (isNotIdentity) Vec3.transformMat4(_imageA, _imageA, imageTransform);
-        if (Vec3.squaredDistance(_imageA, bCenter) > testDistanceSq) continue;
+        v3set(_imageA, xA[aI], yA[aI], zA[aI]);
+        if (isNotIdentity) v3transformMat4(_imageA, _imageA, imageTransform);
+        if (v3squaredDistance(_imageA, bCenter) > testDistanceSq) continue;
 
         if (!props.forceCompute && indexPairs) {
             const { maxDistance } = indexPairs;
-            const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
+            const { offset, b, edgeProps: { order, distance, flag, key } } = indexPairs.bonds;
 
             const srcA = sourceIndex.value(aI);
+            const aeI = getElementIdx(type_symbolA.value(aI));
             for (let i = offset[srcA], il = offset[srcA + 1]; i < il; ++i) {
                 const bI = invertedIndex![b[i]];
 
                 const _bI = SortedArray.indexOf(unitB.elements, bI) as StructureElement.UnitIndex;
                 if (_bI < 0) continue;
 
-                const aeI = getElementIdx(type_symbolA.value(aI));
                 const beI = getElementIdx(type_symbolA.value(bI));
 
                 const d = distance[i];
@@ -107,7 +113,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 }
 
                 if (add) {
-                    builder.add(_aI, _bI, { order: order[i], flag: flag[i] });
+                    builder.add(_aI, _bI, { order: order[i], flag: flag[i], key: key[i] });
                 }
             }
             continue; // assume `indexPairs` supplies all bonds
@@ -125,7 +131,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 // check if the bond is within MAX_RADIUS for this pair of units
                 if (getDistance(unitA, aI, unitB, p.atomIndex) > maxRadius) continue;
 
-                builder.add(_aI, _bI, { order: se.order, flag: se.flags });
+                builder.add(_aI, _bI, { order: se.order, flag: se.flags, key: se.rowIndex });
                 added = true;
             }
             // assume, for an atom, that if any inter unit bond is given
@@ -181,7 +187,8 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
                 const compIdB = label_comp_idB.value(residueIndexB[bI]);
                 builder.add(_aI, _bI, {
                     order: getInterBondOrderFromTable(compIdA, compIdB, atomIdA, atomIdB),
-                    flag: (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed
+                    flag: (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed,
+                    key: -1
                 });
             }
         }
@@ -191,6 +198,7 @@ function findPairBonds(unitA: Unit.Atomic, unitB: Unit.Atomic, props: BondComput
 }
 
 export interface InterBondComputationProps extends BondComputationProps {
+    validUnit: (unit: Unit) => boolean
     validUnitPair: (structure: Structure, unitA: Unit, unitB: Unit) => boolean
     ignoreWater: boolean
     ignoreIon: boolean
@@ -215,7 +223,7 @@ function findBonds(structure: Structure, props: InterBondComputationProps) {
         findPairBonds(unitA as Unit.Atomic, unitB as Unit.Atomic, props, builder);
     }, {
         maxRadius: props.maxRadius,
-        validUnit: (unit: Unit) => Unit.isAtomic(unit),
+        validUnit: (unit: Unit) => props.validUnit(unit),
         validUnitPair: (unitA: Unit, unitB: Unit) => props.validUnitPair(structure, unitA, unitB)
     });
 
@@ -226,6 +234,7 @@ function computeInterUnitBonds(structure: Structure, props?: Partial<InterBondCo
     const p = { ...DefaultInterBondComputationProps, ...props };
     return findBonds(structure, {
         ...p,
+        validUnit: (props && props.validUnit) || (u => Unit.isAtomic(u)),
         validUnitPair: (props && props.validUnitPair) || ((s, a, b) => {
             const mtA = a.model.atomicHierarchy.derived.residue.moleculeType;
             const mtB = b.model.atomicHierarchy.derived.residue.moleculeType;
diff --git a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts
index 105347895b27c9080d7e5daeca079c975cbb6261..29fd505381b21fd4791c49718f3900b8eeb001cb 100644
--- a/src/mol-model/structure/structure/unit/bonds/intra-compute.ts
+++ b/src/mol-model/structure/structure/unit/bonds/intra-compute.ts
@@ -21,17 +21,22 @@ import { ElementIndex } from '../../../model/indexing';
 import { equalEps } from '../../../../../mol-math/linear-algebra/3d/common';
 import { Model } from '../../../model/model';
 
-function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
+// avoiding namespace lookup improved performance in Chrome (Aug 2020)
+const v3distance = Vec3.distance;
+
+function getGraph(atomA: StructureElement.UnitIndex[], atomB: StructureElement.UnitIndex[], _order: number[], _flags: number[], _key: number[], atomCount: number, canRemap: boolean): IntraUnitBonds {
     const builder = new IntAdjacencyGraph.EdgeBuilder(atomCount, atomA, atomB);
     const flags = new Uint16Array(builder.slotCount);
     const order = new Int8Array(builder.slotCount);
+    const key = new Uint32Array(builder.slotCount);
     for (let i = 0, _i = builder.edgeCount; i < _i; i++) {
         builder.addNextEdge();
         builder.assignProperty(flags, _flags[i]);
         builder.assignProperty(order, _order[i]);
+        builder.assignProperty(key, _key[i]);
     }
 
-    return builder.createGraph({ flags, order }, { canRemap });
+    return builder.createGraph({ flags, order, key }, { canRemap });
 }
 
 const tmpDistVecA = Vec3();
@@ -39,7 +44,7 @@ const tmpDistVecB = Vec3();
 function getDistance(unit: Unit.Atomic, indexA: ElementIndex, indexB: ElementIndex) {
     unit.conformation.position(indexA, tmpDistVecA);
     unit.conformation.position(indexB, tmpDistVecB);
-    return Vec3.distance(tmpDistVecA, tmpDistVecB);
+    return v3distance(tmpDistVecA, tmpDistVecB);
 }
 
 const __structConnAdded = new Set<StructureElement.UnitIndex>();
@@ -50,7 +55,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const { type_symbol } = unit.model.atomicHierarchy.atoms;
     const atomCount = unit.elements.length;
     const { maxDistance } = indexPairs;
-    const { offset, b, edgeProps: { order, distance, flag } } = indexPairs.bonds;
+    const { offset, b, edgeProps: { order, distance, flag, key } } = indexPairs.bonds;
 
     const { atomSourceIndex: sourceIndex } = unit.model.atomicHierarchy;
     const { invertedIndex } = Model.getInvertedAtomSourceIndex(unit.model);
@@ -59,6 +64,7 @@ function findIndexPairBonds(unit: Unit.Atomic) {
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
     const orders: number[] = [];
+    const keys: number[] = [];
 
     for (let _aI = 0 as StructureElement.UnitIndex; _aI < atomCount; _aI++) {
         const aI = atoms[_aI];
@@ -101,11 +107,12 @@ function findIndexPairBonds(unit: Unit.Atomic) {
                 atomB[atomB.length] = _bI;
                 orders[orders.length] = order[i];
                 flags[flags.length] = flag[i];
+                keys[keys.length] = key[i];
             }
         }
     }
 
-    return getGraph(atomA, atomB, orders, flags, atomCount, false);
+    return getGraph(atomA, atomB, orders, flags, keys, atomCount, false);
 }
 
 function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBonds {
@@ -129,9 +136,10 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
     const atomB: StructureElement.UnitIndex[] = [];
     const flags: number[] = [];
     const order: number[] = [];
+    const key: number[] = [];
 
     let lastResidue = -1;
-    let componentMap: Map<string, Map<string, { flags: number, order: number }>> | undefined = void 0;
+    let componentMap: Map<string, Map<string, { flags: number, order: number, key: number }>> | undefined = void 0;
 
     let isWatery = true, isDictionaryBased = true, isSequenced = true;
 
@@ -159,6 +167,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                 atomB[atomB.length] = _bI;
                 flags[flags.length] = se.flags;
                 order[order.length] = se.order;
+                key[key.length] = se.rowIndex;
 
                 if (!hasStructConn) structConnAdded.clear();
                 hasStructConn = true;
@@ -227,6 +236,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                         flag |= BondType.Flag.MetallicCoordination;
                     }
                     flags[flags.length] = flag;
+                    key[key.length] = e.key;
                 }
                 continue;
             }
@@ -240,6 +250,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
                 atomB[atomB.length] = _bI;
                 order[order.length] = getIntraBondOrderFromTable(compId, atomIdA, label_atom_id.value(bI));
                 flags[flags.length] = (isMetal ? BondType.Flag.MetallicCoordination : BondType.Flag.Covalent) | BondType.Flag.Computed;
+                key[key.length] = -1;
 
                 const seqIdB = label_seq_id.value(rbI);
 
@@ -250,7 +261,7 @@ function findBonds(unit: Unit.Atomic, props: BondComputationProps): IntraUnitBon
     }
 
     const canRemap = isWatery || (isDictionaryBased && isSequenced);
-    return getGraph(atomA, atomB, order, flags, atomCount, canRemap);
+    return getGraph(atomA, atomB, order, flags, key, atomCount, canRemap);
 }
 
 function computeIntraUnitBonds(unit: Unit.Atomic, props?: Partial<BondComputationProps>) {
diff --git a/src/mol-model/structure/structure/util/lookup3d.ts b/src/mol-model/structure/structure/util/lookup3d.ts
index 49de19cbd20e0ffa6790767068e6f519aabc48fe..693d17aca13aaa4d607ab3826a3a6b834deaca40 100644
--- a/src/mol-model/structure/structure/util/lookup3d.ts
+++ b/src/mol-model/structure/structure/util/lookup3d.ts
@@ -3,6 +3,7 @@
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Gianluca Tomasello <giagitom@gmail.com>
  */
 
 import { Structure } from '../structure';
@@ -13,6 +14,7 @@ import { StructureUniqueSubsetBuilder } from './unique-subset-builder';
 import { StructureElement } from '../element';
 import { Unit } from '../unit';
 import { UnitIndex } from '../element/util';
+import { FibonacciHeap } from '../../../../mol-util/fibonacci-heap';
 
 export interface StructureResult extends Result<StructureElement.UnitIndex> {
     units: Unit[]
@@ -54,6 +56,7 @@ export function StructureLookup3DResultContext(): StructureLookup3DResultContext
 export class StructureLookup3D {
     private unitLookup: Lookup3D;
     private pivot = Vec3();
+    private heap = new FibonacciHeap();
 
     findUnitIndices(x: number, y: number, z: number, radius: number): Result<number> {
         return this.unitLookup.find(x, y, z, radius);
@@ -86,6 +89,54 @@ export class StructureLookup3D {
         return ctx.result;
     }
 
+    nearest(x: number, y: number, z: number, k: number = 1, ctx?: StructureLookup3DResultContext): StructureResult {
+        return this._nearest(x, y, z, k, ctx ?? this.findContext);
+    }
+
+    _nearest(x: number, y: number, z: number, k: number, ctx: StructureLookup3DResultContext): StructureResult {
+        const result = ctx.result, heap = this.heap;
+        Result.reset(result);
+        heap.clear();
+        const { units } = this.structure;
+        let elementsCount = 0;
+        const closeUnits = this.unitLookup.nearest(x, y, z, units.length, (uid: number) => (elementsCount += units[uid].elements.length) >= k, ctx.closeUnitsResult); // sort units based on distance to the point
+        if (closeUnits.count === 0) return result;
+        let totalCount = 0, maxDistResult = -Number.MAX_VALUE;
+        for (let t = 0, _t = closeUnits.count; t < _t; t++) {
+            const unitSqDist = closeUnits.squaredDistances[t];
+            if (totalCount >= k && maxDistResult < unitSqDist) break;
+            Vec3.set(this.pivot, x, y, z);
+            const unit = units[closeUnits.indices[t]];
+            if (!unit.conformation.operator.isIdentity) {
+                Vec3.transformMat4(this.pivot, this.pivot, unit.conformation.operator.inverse);
+            }
+            const unitLookup = unit.lookup3d;
+            const groupResult = unitLookup.nearest(this.pivot[0], this.pivot[1], this.pivot[2], k, void 0, ctx.unitGroupResult);
+            if (groupResult.count === 0) continue;
+            totalCount += groupResult.count;
+            maxDistResult = Math.max(maxDistResult, groupResult.squaredDistances[groupResult.count - 1]);
+            for (let j = 0, _j = groupResult.count; j < _j; j++) {
+                heap.insert(groupResult.squaredDistances[j], { index: groupResult.indices[j], unit: unit });
+            }
+        }
+        if (k === 1) {
+            const node = heap.findMinimum();
+            if (node) {
+                const { key: squaredDistance } = node;
+                const { unit, index } = node.value as { index: UnitIndex, unit: Unit };
+                StructureResult.add(result, unit as Unit, index as UnitIndex, squaredDistance as number);
+            }
+        } else {
+            while (!heap.isEmpty() && result.count < k) {
+                const node = heap.extractMinimum();
+                const { key: squaredDistance } = node!;
+                const { unit, index } = node!.value as { index: UnitIndex, unit: Unit };
+                StructureResult.add(result, unit as Unit, index as UnitIndex, squaredDistance as number);
+            }
+        }
+        return result;
+    }
+
     findIntoBuilder(x: number, y: number, z: number, radius: number, builder: StructureUniqueSubsetBuilder) {
         const { units } = this.structure;
         const closeUnits = this.unitLookup.find(x, y, z, radius);
@@ -217,4 +268,4 @@ export class StructureLookup3D {
         const position = { x: xs, y: ys, z: zs, radius, indices: OrderedSet.ofBounds(0, unitCount) };
         this.unitLookup = GridLookup3D(position, boundary);
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-model/structure/structure/util/superposition-sifts-mapping.ts b/src/mol-model/structure/structure/util/superposition-sifts-mapping.ts
index 4abf51e51762533338aad2ce4fe2f73e41d63c52..48fec9d0bd6fa744c5a4e701b56d19a659cd39ed 100644
--- a/src/mol-model/structure/structure/util/superposition-sifts-mapping.ts
+++ b/src/mol-model/structure/structure/util/superposition-sifts-mapping.ts
@@ -8,7 +8,8 @@
 import { Segmentation } from '../../../../mol-data/int';
 import { MinimizeRmsd } from '../../../../mol-math/linear-algebra/3d/minimize-rmsd';
 import { SIFTSMapping } from '../../../../mol-model-props/sequence/sifts-mapping';
-import { ElementIndex } from '../../model/indexing';
+import { ElementIndex, ResidueIndex } from '../../model/indexing';
+import { StructureElement } from '../element';
 import { Structure } from '../structure';
 import { Unit } from '../unit';
 
@@ -24,11 +25,16 @@ export interface AlignmentResult {
     failedPairs: [number, number][]
 }
 
-export function alignAndSuperposeWithSIFTSMapping(structures: Structure[], options?: { traceOnly?: boolean }): AlignmentResult {
+type IncludeResidueTest = (traceElementOrFirstAtom: StructureElement.Location<Unit.Atomic>, residueIndex: ResidueIndex, startIndex: ElementIndex, endIndex: ElementIndex) => boolean
+
+export function alignAndSuperposeWithSIFTSMapping(
+    structures: Structure[],
+    options?: { traceOnly?: boolean, includeResidueTest?: IncludeResidueTest }
+): AlignmentResult {
     const indexMap = new Map<string, IndexEntry>();
 
     for (let i = 0; i < structures.length; i++) {
-        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true);
+        buildIndex(structures[i], indexMap, i, options?.traceOnly ?? true, options?.includeResidueTest ?? _includeAllResidues);
     }
 
     const index = Array.from(indexMap.values());
@@ -137,11 +143,16 @@ interface IndexEntry {
     pivots: { [i: number]: [unit: Unit.Atomic, start: ElementIndex, end: ElementIndex] | undefined }
 }
 
-function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean) {
+function _includeAllResidues() { return true; }
+
+function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: number, traceOnly: boolean, includeTest: IncludeResidueTest) {
+    const loc = StructureElement.Location.create<Unit.Atomic>(structure);
+
     for (const unit of structure.units) {
         if (unit.kind !== Unit.Kind.Atomic) continue;
 
         const { elements, model } = unit;
+        loc.unit = unit;
 
         const map = SIFTSMapping.Provider.get(model).value;
         if (!map) return;
@@ -161,9 +172,11 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
 
                 if (!dbName[rI]) continue;
 
+                const traceElement = traceElementIndex[rI];
+
                 let start, end;
                 if (traceOnly) {
-                    start = traceElementIndex[rI];
+                    start = traceElement;
                     if (start === -1) continue;
                     end = start + 1 as ElementIndex;
                 } else {
@@ -171,6 +184,9 @@ function buildIndex(structure: Structure, index: Map<string, IndexEntry>, sI: nu
                     end = elements[residueSegment.end - 1] + 1 as ElementIndex;
                 }
 
+                loc.element = (traceElement >= 0 ? traceElement : start) as ElementIndex;
+                if (!includeTest(loc, rI, start, end)) continue;
+
                 const key = `${dbName[rI]}-${accession[rI]}-${num[rI]}`;
 
                 if (!index.has(key)) {
diff --git a/src/mol-model/volume/volume.ts b/src/mol-model/volume/volume.ts
index 8a322563ae06817f174312c55f413357baded5a1..741f51864665d8225117d11fda6117cb9efb1fa0 100644
--- a/src/mol-model/volume/volume.ts
+++ b/src/mol-model/volume/volume.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -126,12 +126,12 @@ export namespace Volume {
                 'absolute': PD.Converted(
                     (v: Volume.IsoValue) => Volume.IsoValue.toAbsolute(v, Grid.One.stats).absoluteValue,
                     (v: number) => Volume.IsoValue.absolute(v),
-                    PD.Numeric(mean, { min, max, step: toPrecision(sigma / 100, 2) })
+                    PD.Numeric(mean, { min, max, step: toPrecision(sigma / 100, 2) }, { immediateUpdate: true })
                 ),
                 'relative': PD.Converted(
                     (v: Volume.IsoValue) => Volume.IsoValue.toRelative(v, Grid.One.stats).relativeValue,
                     (v: number) => Volume.IsoValue.relative(v),
-                    PD.Numeric(Math.min(1, relMax), { min: relMin, max: relMax, step: toPrecision(Math.round(((max - min) / sigma)) / 100, 2) })
+                    PD.Numeric(Math.min(1, relMax), { min: relMin, max: relMax, step: toPrecision(Math.round(((max - min) / sigma)) / 100, 2) }, { immediateUpdate: true })
                 )
             },
             (v: Volume.IsoValue) => v.kind === 'absolute' ? 'absolute' : 'relative',
@@ -219,4 +219,14 @@ export namespace Volume {
             return Sphere3D.expand(bs, bs, Mat4.getMaxScaleOnAxis(transform) * 10);
         }
     }
+
+    export type PickingGranularity = 'volume' | 'object' | 'voxel';
+    export const PickingGranularity = {
+        set(volume: Volume, granularity: PickingGranularity) {
+            volume._propertyData['__picking_granularity__'] = granularity;
+        },
+        get(volume: Volume): PickingGranularity {
+            return volume._propertyData['__picking_granularity__'] ?? 'voxel';
+        }
+    };
 }
\ No newline at end of file
diff --git a/src/mol-plugin-state/actions/file.ts b/src/mol-plugin-state/actions/file.ts
index 0f92b915b180d8626daf8cab3a1e460bfc19965a..ee81b0793d6aae3808e1a467f75ab1fc00393dfe 100644
--- a/src/mol-plugin-state/actions/file.ts
+++ b/src/mol-plugin-state/actions/file.ts
@@ -83,7 +83,7 @@ export const DownloadFile = StateAction.build({
     display: { name: 'Download File', description: 'Load one or more file from an URL' },
     from: PluginStateObject.Root,
     params: (a, ctx: PluginContext) => {
-        const options = [...ctx.dataFormats.options, ['zip', 'Zip'] as const];
+        const options = [...ctx.dataFormats.options, ['zip', 'Zip'] as const, ['gzip', 'Gzip'] as const];
         return {
             url: PD.Url(''),
             format: PD.Select(options[0][0], options),
@@ -96,17 +96,23 @@ export const DownloadFile = StateAction.build({
 
     await state.transaction(async () => {
         try {
-            if (params.format === 'zip') {
+            if (params.format === 'zip' || params.format === 'gzip') {
                 // TODO: add ReadZipFile transformer so this can be saved as a simple state snaphot,
                 //       would need support for extracting individual files from zip
                 const data = await plugin.builders.data.download({ url: params.url, isBinary: true });
-                const zippedFiles = await unzip(taskCtx, (data.obj?.data as Uint8Array).buffer);
-                for (const [fn, filedata] of Object.entries(zippedFiles)) {
-                    if (!(filedata instanceof Uint8Array) || filedata.length === 0) continue;
+                if (params.format === 'zip') {
+                    const zippedFiles = await unzip(taskCtx, (data.obj?.data as Uint8Array).buffer);
+                    for (const [fn, filedata] of Object.entries(zippedFiles)) {
+                        if (!(filedata instanceof Uint8Array) || filedata.length === 0) continue;
 
-                    const asset = Asset.File(new File([filedata], fn));
+                        const asset = Asset.File(new File([filedata], fn));
 
-                    await processFile(asset, plugin, 'auto', params.visuals);
+                        await processFile(asset, plugin, 'auto', params.visuals);
+                    }
+                } else {
+                    const url = Asset.getUrl(params.url);
+                    const info = getFileInfo(url);
+                    await processFile(Asset.File(new File([data.obj?.data as Uint8Array], info.name)), plugin, 'auto', params.visuals);
                 }
             } else {
                 const provider = plugin.dataFormats.get(params.format);
diff --git a/src/mol-plugin-state/actions/structure.ts b/src/mol-plugin-state/actions/structure.ts
index 630c5d0cdf9f706e3092dd585b217cbb5e29caaa..61765b5a22b2ebc3d3b3592e98c5f0a48357fd20 100644
--- a/src/mol-plugin-state/actions/structure.ts
+++ b/src/mol-plugin-state/actions/structure.ts
@@ -90,6 +90,7 @@ const DownloadStructure = StateAction.build({
                     url: PD.Url(''),
                     format: PD.Select<BuiltInTrajectoryFormat>('mmcif', PD.arrayToOptions(BuiltInTrajectoryFormats.map(f => f[0]), f => f)),
                     isBinary: PD.Boolean(false),
+                    label: PD.Optional(PD.Text('')),
                     options
                 }, { isFlat: true, label: 'URL' })
             })
@@ -104,7 +105,7 @@ const DownloadStructure = StateAction.build({
 
     switch (src.name) {
         case 'url':
-            downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary }];
+            downloadParams = [{ url: src.params.url, isBinary: src.params.isBinary, label: src.params.label || undefined }];
             format = src.params.format;
             break;
         case 'pdb':
diff --git a/src/mol-plugin-state/builder/structure/hierarchy-preset.ts b/src/mol-plugin-state/builder/structure/hierarchy-preset.ts
index b4b531e1585bb4057ae7a544c4949893f8beee8c..320df11dfe925d31383e342a7b6b1215001d508c 100644
--- a/src/mol-plugin-state/builder/structure/hierarchy-preset.ts
+++ b/src/mol-plugin-state/builder/structure/hierarchy-preset.ts
@@ -86,7 +86,7 @@ const allModels = TrajectoryHierarchyPresetProvider({
     id: 'preset-trajectory-all-models',
     display: {
         name: 'All Models', group: 'Preset',
-        description: 'Shows all models; colored by model-index.'
+        description: 'Shows all models; colored by trajectory-index.'
     },
     isApplicable: o => {
         return o.data.frameCount > 1;
@@ -115,7 +115,7 @@ const allModels = TrajectoryHierarchyPresetProvider({
 
             const quality = structure.obj ? getStructureQuality(structure.obj.data, { elementCountFactor: tr.frameCount }) : 'medium';
             const representationPreset = params.representationPreset || plugin.config.get(PluginConfig.Structure.DefaultRepresentationPreset) || PresetStructureRepresentations.auto.id;
-            await builder.representation.applyPreset(structureProperties, representationPreset, { theme: { globalName: 'model-index' }, quality });
+            await builder.representation.applyPreset(structureProperties, representationPreset, { theme: { globalName: 'trajectory-index' }, quality });
         }
 
         return { models, structures };
diff --git a/src/mol-plugin-state/builder/structure/representation-preset.ts b/src/mol-plugin-state/builder/structure/representation-preset.ts
index b581666d5548291d6082590eca1fadae460ccd94..25f2d1f907f1772b52a9da01ff27d0d91df9bfad 100644
--- a/src/mol-plugin-state/builder/structure/representation-preset.ts
+++ b/src/mol-plugin-state/builder/structure/representation-preset.ts
@@ -41,8 +41,10 @@ export namespace StructureRepresentationPresetProvider {
         quality: PD.Optional(PD.Select<VisualQuality>('auto', VisualQualityOptions)),
         theme: PD.Optional(PD.Group({
             globalName: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
+            globalColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
             carbonColor: PD.Optional(PD.Select('chain-id', PD.arrayToOptions(['chain-id', 'operator-name', 'element-symbol'] as const))),
             symmetryColor: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
+            symmetryColorParams: PD.Optional(PD.Value<any>({}, { isHidden: true })),
             focus: PD.Optional(PD.Group({
                 name: PD.Optional(PD.Text<ColorTheme.BuiltIn>('')),
                 params: PD.Optional(PD.Value<ColorTheme.BuiltInParams<ColorTheme.BuiltIn>>({} as any))
@@ -76,13 +78,15 @@ export namespace StructureRepresentationPresetProvider {
         if (params.ignoreLight !== void 0) typeParams.ignoreLight = !!params.ignoreLight;
         const color: ColorTheme.BuiltIn | undefined = params.theme?.globalName ? params.theme?.globalName : void 0;
         const ballAndStickColor: ColorTheme.BuiltInParams<'element-symbol'> = params.theme?.carbonColor !== undefined
-            ? { carbonColor: getCarbonColorParams(params.theme?.carbonColor) }
-            : { };
+            ? { carbonColor: getCarbonColorParams(params.theme?.carbonColor), ...params.theme?.globalColorParams }
+            : { ...params.theme?.globalColorParams };
         const symmetryColor: ColorTheme.BuiltIn | undefined = structure && params.theme?.symmetryColor
             ? isSymmetry(structure) ? params.theme?.symmetryColor : color
             : color;
+        const symmetryColorParams = params.theme?.symmetryColorParams ? { ...params.theme?.globalColorParams, ...params.theme?.symmetryColorParams } : { ...params.theme?.globalColorParams };
+        const globalColorParams = params.theme?.globalColorParams ? { ...params.theme?.globalColorParams } : undefined;
 
-        return { update, builder, color, symmetryColor, typeParams, ballAndStickColor };
+        return { update, builder, color, symmetryColor, symmetryColorParams, globalColorParams, typeParams, ballAndStickColor };
     }
 
     export function updateFocusRepr<T extends ColorTheme.BuiltIn>(plugin: PluginContext, structure: Structure, themeName: T | undefined, themeParams: ColorTheme.BuiltInParams<T> | undefined) {
@@ -177,18 +181,18 @@ const polymerAndLigand = StructureRepresentationPresetProvider({
         const waterType = (components.water?.obj?.data?.elementCount || 0) > 50_000 ? 'line' : 'ball-and-stick';
         const lipidType = (components.lipid?.obj?.data?.elementCount || 0) > 20_000 ? 'line' : 'ball-and-stick';
 
-        const { update, builder, typeParams, color, symmetryColor, ballAndStickColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, color, symmetryColor, symmetryColorParams, globalColorParams, ballAndStickColor } = reprBuilder(plugin, params, structure);
 
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' }),
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
             ligand: builder.buildRepresentation(update, components.ligand, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'ligand' }),
             nonStandard: builder.buildRepresentation(update, components.nonStandard, { type: 'ball-and-stick', typeParams, color, colorParams: ballAndStickColor }, { tag: 'non-standard' }),
             branchedBallAndStick: builder.buildRepresentation(update, components.branched, { type: 'ball-and-stick', typeParams: { ...typeParams, alpha: 0.3 }, color, colorParams: ballAndStickColor }, { tag: 'branched-ball-and-stick' }),
-            branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color }, { tag: 'branched-snfg-3d' }),
-            water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'water' }),
-            ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'ion' }),
-            lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} } } }, { tag: 'lipid' }),
-            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id' }, { tag: 'coarse' })
+            branchedSnfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams, color, colorParams: globalColorParams }, { tag: 'branched-snfg-3d' }),
+            water: builder.buildRepresentation(update, components.water, { type: waterType, typeParams: { ...typeParams, alpha: 0.6, visuals: waterType === 'line' ? ['intra-bond', 'element-point'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'water' }),
+            ion: builder.buildRepresentation(update, components.ion, { type: 'ball-and-stick', typeParams, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'ion' }),
+            lipid: builder.buildRepresentation(update, components.lipid, { type: lipidType, typeParams: { ...typeParams, alpha: 0.6, visuals: lipidType === 'line' ? ['intra-bond'] : undefined }, color, colorParams: { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams } }, { tag: 'lipid' }),
+            coarse: builder.buildRepresentation(update, components.coarse, { type: 'spacefill', typeParams, color: color || 'chain-id', colorParams: globalColorParams }, { tag: 'coarse' })
         };
 
         await update.commit({ revertOnError: false });
@@ -223,11 +227,11 @@ const proteinAndNucleic = StructureRepresentationPresetProvider({
             smoothness: structure.isCoarseGrained ? 1.0 : 1.5,
         };
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
         const representations = {
-            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'protein' }),
-            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'nucleic' })
+            protein: builder.buildRepresentation(update, components.protein, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'protein' }),
+            nucleic: builder.buildRepresentation(update, components.nucleic, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'nucleic' })
         };
 
         await update.commit({ revertOnError: true });
@@ -275,11 +279,11 @@ const coarseSurface = StructureRepresentationPresetProvider({
             });
         }
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'polymer' }),
-            lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor }, { tag: 'lipid' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' }),
+            lipid: builder.buildRepresentation(update, components.lipid, { type: 'gaussian-surface', typeParams: { ...typeParams, ...gaussianProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'lipid' })
         };
 
         await update.commit({ revertOnError: true });
@@ -309,10 +313,10 @@ const polymerCartoon = StructureRepresentationPresetProvider({
             sizeFactor: structure.isCoarseGrained ? 0.8 : 0.2
         };
 
-        const { update, builder, typeParams, symmetryColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, symmetryColor, symmetryColorParams } = reprBuilder(plugin, params, structure);
 
         const representations = {
-            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor }, { tag: 'polymer' })
+            polymer: builder.buildRepresentation(update, components.polymer, { type: 'cartoon', typeParams: { ...typeParams, ...cartoonProps }, color: symmetryColor, colorParams: symmetryColorParams }, { tag: 'polymer' })
         };
 
         await update.commit({ revertOnError: true });
@@ -367,9 +371,9 @@ const atomicDetail = StructureRepresentationPresetProvider({
             });
         }
 
-        const { update, builder, typeParams, color, ballAndStickColor } = reprBuilder(plugin, params, structure);
+        const { update, builder, typeParams, color, ballAndStickColor, globalColorParams } = reprBuilder(plugin, params, structure);
         const colorParams = lowResidueElementRatio && !bondsGiven
-            ? { carbonColor: { name: 'element-symbol', params: {} } }
+            ? { carbonColor: { name: 'element-symbol', params: {} }, ...globalColorParams }
             : ballAndStickColor;
 
         const representations = {
@@ -377,7 +381,7 @@ const atomicDetail = StructureRepresentationPresetProvider({
         };
         if (showCarbohydrateSymbol) {
             Object.assign(representations, {
-                snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color }, { tag: 'snfg-3d' }),
+                snfg3d: builder.buildRepresentation(update, components.branched, { type: 'carbohydrate', typeParams: { ...typeParams, alpha: 0.4, visuals: ['carbohydrate-symbol'] }, color, colorParams: globalColorParams }, { tag: 'snfg-3d' }),
             });
         }
 
diff --git a/src/mol-plugin-state/builder/structure/representation.ts b/src/mol-plugin-state/builder/structure/representation.ts
index fc2b05c029131622f3093d7c8de70b87e341cd2e..4bcbb686140f517c5cceda8f53f63ed0d14c5e6f 100644
--- a/src/mol-plugin-state/builder/structure/representation.ts
+++ b/src/mol-plugin-state/builder/structure/representation.ts
@@ -90,7 +90,7 @@ export class StructureRepresentationBuilder {
     }
 
     applyPreset<K extends keyof PresetStructureRepresentations>(parent: StateObjectRef<PluginStateObject.Molecule.Structure>, preset: K, params?: StructureRepresentationPresetProvider.Params<PresetStructureRepresentations[K]>): Promise<StructureRepresentationPresetProvider.State<PresetStructureRepresentations[K]>> | undefined
-    applyPreset<P = any, S = {}>(parent: StateObjectRef<PluginStateObject.Molecule.Structure>, provider: StructureRepresentationPresetProvider<P, S>, params?: P): Promise<S> | undefined
+    applyPreset<P = any, S extends {} = {}>(parent: StateObjectRef<PluginStateObject.Molecule.Structure>, provider: StructureRepresentationPresetProvider<P, S>, params?: P): Promise<S> | undefined
     applyPreset(parent: StateObjectRef<PluginStateObject.Molecule.Structure>, providerId: string, params?: any): Promise<any> | undefined
     applyPreset(parent: StateObjectRef, providerRef: string | StructureRepresentationPresetProvider, params?: any): Promise<any> | undefined {
         const provider = this.resolveProvider(providerRef);
diff --git a/src/mol-plugin-state/component.ts b/src/mol-plugin-state/component.ts
index 8230b7f222e48a85b0b350bed83cd11db8ebb759..46b7c35077efc787d5bc7a96b507a10d7ffe0a09 100644
--- a/src/mol-plugin-state/component.ts
+++ b/src/mol-plugin-state/component.ts
@@ -42,7 +42,7 @@ export class PluginComponent {
     }
 }
 
-export class StatefulPluginComponent<State> extends PluginComponent {
+export class StatefulPluginComponent<State extends {}> extends PluginComponent {
     private _state: State;
 
     protected updateState(...states: Partial<State>[]): boolean {
diff --git a/src/mol-plugin-state/formats/trajectory.ts b/src/mol-plugin-state/formats/trajectory.ts
index c8eca7e90416533fce2aa6c0605d0016c2521c61..82eef87085d9389dc63800c0427822f8505b1b7a 100644
--- a/src/mol-plugin-state/formats/trajectory.ts
+++ b/src/mol-plugin-state/formats/trajectory.ts
@@ -75,7 +75,7 @@ export const CifCoreProvider: TrajectoryFormatProvider = {
     visuals: defaultVisuals
 };
 
-function directTrajectory<P>(transformer: StateTransformer<PluginStateObject.Data.String | PluginStateObject.Data.Binary, PluginStateObject.Molecule.Trajectory, P>, transformerParams?: P): TrajectoryFormatProvider['parse'] {
+function directTrajectory<P extends {}>(transformer: StateTransformer<PluginStateObject.Data.String | PluginStateObject.Data.Binary, PluginStateObject.Molecule.Trajectory, P>, transformerParams?: P): TrajectoryFormatProvider['parse'] {
     return async (plugin, data, params) => {
         const state = plugin.state.data;
         const trajectory = await state.build().to(data)
diff --git a/src/mol-plugin-state/transforms/model.ts b/src/mol-plugin-state/transforms/model.ts
index e9b356ddcd6fca4c285926139203ce8411fb68b6..3f1fff5864b424c68583e6e96672cbeed7d7d3d8 100644
--- a/src/mol-plugin-state/transforms/model.ts
+++ b/src/mol-plugin-state/transforms/model.ts
@@ -1079,4 +1079,4 @@ const ShapeFromPly = PluginStateTransform.BuiltIn({
             return new SO.Shape.Provider(shape, props);
         });
     }
-});
\ No newline at end of file
+});
diff --git a/src/mol-plugin-ui/base.tsx b/src/mol-plugin-ui/base.tsx
index 6311d351b19aefa6ff5a5e0ef6dd92a10a71c396..6d724fa66726db3ba8141f1e548e1bd217ec85be 100644
--- a/src/mol-plugin-ui/base.tsx
+++ b/src/mol-plugin-ui/base.tsx
@@ -13,7 +13,7 @@ import { Icon, ArrowRightSvg, ArrowDropDownSvg } from './controls/icons';
 
 export const PluginReactContext = React.createContext(void 0 as any as PluginUIContext);
 
-export abstract class PluginUIComponent<P = {}, S = {}, SS = {}> extends React.Component<P & { children?: any }, S, SS> {
+export abstract class PluginUIComponent<P extends {} = {}, S = {}, SS = {}> extends React.Component<P & { children?: any }, S, SS> {
     static contextType = PluginReactContext;
     readonly plugin: PluginUIContext;
 
diff --git a/src/mol-plugin-ui/controls/parameters.tsx b/src/mol-plugin-ui/controls/parameters.tsx
index ac908aeccc687e434a9a4bd1306082b156633f19..4a59801972ee5f62b538f035d5bd5560cb676646 100644
--- a/src/mol-plugin-ui/controls/parameters.tsx
+++ b/src/mol-plugin-ui/controls/parameters.tsx
@@ -7,6 +7,7 @@
 
 import * as React from 'react';
 import { Mat4, Vec2, Vec3 } from '../../mol-math/linear-algebra';
+import { Script } from '../../mol-script/script';
 import { Asset } from '../../mol-util/assets';
 import { Color } from '../../mol-util/color';
 import { ColorListEntry } from '../../mol-util/color/color';
@@ -22,7 +23,7 @@ import { PluginUIContext } from '../context';
 import { ActionMenu } from './action-menu';
 import { ColorOptions, ColorValueOption, CombinedColorControl } from './color';
 import { Button, ControlGroup, ControlRow, ExpandGroup, IconButton, TextInput, ToggleButton } from './common';
-import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg } from './icons';
+import { ArrowDownwardSvg, ArrowDropDownSvg, ArrowRightSvg, ArrowUpwardSvg, BookmarksOutlinedSvg, CheckSvg, ClearSvg, DeleteOutlinedSvg, HelpOutlineSvg, Icon, MoreHorizSvg, WarningSvg } from './icons';
 import { legendFor } from './legend';
 import { LineGraphComponent } from './line-graph/line-graph-component';
 import { Slider, Slider2 } from './slider';
@@ -1466,31 +1467,38 @@ export class ConvertedControl extends React.PureComponent<ParamProps<PD.Converte
     }
 }
 
-export class ScriptControl extends SimpleParam<PD.Script> {
-    onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
-        const value = e.target.value;
-        if (value !== this.props.value.expression) {
-            this.update({ language: this.props.value.language, expression: value });
-        }
-    };
-
-    onKeyPress = (e: React.KeyboardEvent<HTMLInputElement>) => {
-        if ((e.keyCode === 13 || e.charCode === 13 || e.key === 'Enter')) {
-            if (this.props.onEnter) this.props.onEnter();
+export class ScriptControl extends React.PureComponent<ParamProps<PD.Script>> {
+    onChange: ParamOnChange = ({ name, value }) => {
+        const k = name as 'language' | 'expression';
+        if (value !== this.props.value[k]) {
+            this.props.onChange({ param: this.props.param, name: this.props.name, value: { ...this.props.value, [k]: value } });
         }
-        e.stopPropagation();
     };
 
-    renderControl() {
+    render() {
         // TODO: improve!
 
-        const placeholder = this.props.param.label || camelCaseToWords(this.props.name);
-        return <input type='text'
-            value={this.props.value.expression || ''}
-            placeholder={placeholder}
-            onChange={this.onChange}
-            onKeyPress={this.props.onEnter ? this.onKeyPress : void 0}
-            disabled={this.props.isDisabled}
-        />;
+        const selectParam: PD.Select<PD.Script['defaultValue']['language']> = {
+            defaultValue: this.props.value.language,
+            options: PD.objectToOptions(Script.Info),
+            type: 'select',
+        };
+        const select = <SelectControl param={selectParam}
+            isDisabled={this.props.isDisabled} onChange={this.onChange} onEnter={this.props.onEnter}
+            name='language' value={this.props.value.language} />;
+
+        const textParam: PD.Text = {
+            defaultValue: this.props.value.language,
+            type: 'text',
+        };
+        const text = <TextControl param={textParam} isDisabled={this.props.isDisabled} onChange={this.onChange} name='expression' value={this.props.value.expression} />;
+
+        return <>
+            {select}
+            {this.props.value.language !== 'mol-script' && <div className='msp-help-text' style={{ padding: '10px' }}>
+                <Icon svg={WarningSvg} /> Support for PyMOL, VMD, and Jmol selections is an experimental feature and may not always work as intended.
+            </div>}
+            {text}
+        </>;
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-plugin-ui/controls/screenshot.tsx b/src/mol-plugin-ui/controls/screenshot.tsx
index a90e0778da21ac8b193541c2f222d38275ebcdee..3d79a786f49152bb3ca050d02b244acb35dd9226 100644
--- a/src/mol-plugin-ui/controls/screenshot.tsx
+++ b/src/mol-plugin-ui/controls/screenshot.tsx
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 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>
  */
 
 import * as React from 'react';
@@ -25,7 +26,7 @@ export interface ScreenshotPreviewProps {
 const _ScreenshotPreview = (props: ScreenshotPreviewProps) => {
     const { plugin, cropFrameColor } = props;
 
-    const helper = plugin.helpers.viewportScreenshot!;
+    const helper = plugin.helpers.viewportScreenshot;
     const [currentCanvas, setCurrentCanvas] = useState<HTMLCanvasElement | null>(null);
     const canvasRef = useRef<HTMLCanvasElement | null>(null);
     const propsRef = useRef(props);
@@ -70,8 +71,8 @@ const _ScreenshotPreview = (props: ScreenshotPreviewProps) => {
         subscribe(plugin.state.data.behaviors.isUpdating, v => {
             if (!v) isDirty = true;
         });
-        subscribe(helper.behaviors.values, () => isDirty = true);
-        subscribe(helper.behaviors.cropParams, () => isDirty = true);
+        subscribe(helper?.behaviors.values, () => isDirty = true);
+        subscribe(helper?.behaviors.cropParams, () => isDirty = true);
 
         let resizeObserver: any = void 0;
         if (typeof ResizeObserver !== 'undefined') {
@@ -108,7 +109,9 @@ export const ScreenshotPreview = React.memo(_ScreenshotPreview, (prev, next) =>
 
 declare const ResizeObserver: any;
 
-function drawPreview(helper: ViewportScreenshotHelper, target: HTMLCanvasElement, customBackground?: string, borderColor?: string, borderWidth?: number) {
+function drawPreview(helper: ViewportScreenshotHelper | undefined, target: HTMLCanvasElement, customBackground?: string, borderColor?: string, borderWidth?: number) {
+    if (!helper) return;
+
     const { canvas, width, height } = helper.getPreview()!;
     const ctx = target.getContext('2d');
     if (!ctx) return;
@@ -151,9 +154,9 @@ function drawPreview(helper: ViewportScreenshotHelper, target: HTMLCanvasElement
 
 function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: { plugin: PluginContext, canvas: HTMLCanvasElement | null, color?: string }) {
     const helper = plugin.helpers.viewportScreenshot;
-    const params = useBehavior(helper?.behaviors.values!);
-    const cropParams = useBehavior(helper?.behaviors.cropParams!);
-    const crop = useBehavior(helper?.behaviors.relativeCrop!);
+    const params = useBehavior(helper?.behaviors.values);
+    const cropParams = useBehavior(helper?.behaviors.cropParams);
+    const crop = useBehavior(helper?.behaviors.relativeCrop);
     const cropFrameRef = useRef<Viewport>({ x: 0, y: 0, width: 0, height: 0 });
     useBehavior(params?.resolution.name === 'viewport' ? plugin.canvas3d?.resized : void 0);
 
@@ -161,7 +164,7 @@ function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: {
     const [start, setStart] = useState([0, 0]);
     const [current, setCurrent] = useState([0, 0]);
 
-    if (!helper || !canvas) return null;
+    if (!helper || !canvas || !crop) return null;
 
     const { width, height } = helper.getSizeAndViewport();
 
@@ -267,7 +270,7 @@ function ViewportFrame({ plugin, canvas, color = 'rgba(255, 87, 45, 0.75)' }: {
 
     function finish() {
         const cropFrame = cropFrameRef.current;
-        if (cropParams.auto) {
+        if (cropParams?.auto) {
             helper?.behaviors.cropParams.next({ ...cropParams, auto: false });
         }
         helper?.behaviors.relativeCrop.next({
diff --git a/src/mol-plugin-ui/custom/volume.tsx b/src/mol-plugin-ui/custom/volume.tsx
index 91e44ea89755e6e13d71a227bed5a64f14a79dd7..0a13a436e087795313b51dd9ac5999822b4df229 100644
--- a/src/mol-plugin-ui/custom/volume.tsx
+++ b/src/mol-plugin-ui/custom/volume.tsx
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Adam Midlik <midlik@gmail.com>
  */
 
 import { PluginUIComponent } from '../base';
@@ -104,7 +105,7 @@ class Channel extends PluginUIComponent<{
             colorStripe={channel.color}
             pivot={<div className='msp-volume-channel-inline-controls'>
                 <Slider value={value} min={ctrlMin} max={ctrlMax} step={step}
-                    onChange={v => props.changeIso(props.name, v, isRelative)} disabled={props.params.isDisabled} onEnter={props.params.events.onEnter} />
+                    onChange={v => props.changeIso(props.name, v, isRelative)} onChangeImmediate={v => props.changeIso(props.name, v, isRelative)} disabled={props.params.isDisabled} onEnter={props.params.events.onEnter} />
                 <IconButton svg={this.getVisible() ? VisibilityOutlinedSvg : VisibilityOffOutlinedSvg} onClick={this.toggleVisible} toggleState={false} disabled={props.params.isDisabled} />
             </div>}
             controls={<ParameterControls onChange={({ name, value }) => props.changeParams(props.name, name, value)} params={ChannelParams} values={channel} onEnter={props.params.events.onEnter} isDisabled={props.params.isDisabled} />}
@@ -199,6 +200,9 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
             const viewParams = { ...oldView };
             if (value.name === 'selection-box') {
                 viewParams.radius = value.params.radius;
+            } else if (value.name === 'camera-target') {
+                viewParams.radius = value.params.radius;
+                viewParams.dynamicDetailLevel = value.params.dynamicDetailLevel;
             } else if (value.name === 'box') {
                 viewParams.bottomLeft = value.params.bottomLeft;
                 viewParams.topRight = value.params.topRight;
@@ -240,13 +244,23 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
         const pivot = isEM ? 'em' : '2fo-fc';
 
         const params = this.props.params as VolumeStreaming.Params;
-        const entry = ((this.props.info.params as VolumeStreaming.ParamDefinition)
-            .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>);
+        const entry = (this.props.info.params as VolumeStreaming.ParamDefinition)
+            .entry.map(params.entry.name) as PD.Group<VolumeStreaming.EntryParamDefinition>;
         const detailLevel = entry.params.detailLevel;
-        const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
+        const dynamicDetailLevel = {
+            ...detailLevel,
+            label: 'Dynamic Detail',
+            defaultValue: (entry.params.view as any).map('camera-target').params.dynamicDetailLevel.defaultValue,
+        };
+        const selectionDetailLevel = {
+            ...detailLevel,
+            label: 'Selection Detail',
+            defaultValue: (entry.params.view as any).map('auto').params.selectionDetailLevel.defaultValue,
+        };
 
         const sampling = b.info.header.sampling[0];
 
+        const isRelative = ((params.entry.params.channels as any)[pivot].isoValue as Volume.IsoValue).kind === 'relative';
         const isRelativeParam = PD.Boolean(isRelative, { description: 'Use normalized or absolute isocontour scale.', label: 'Normalized' });
 
         const isUnbounded = !!(params.entry.params.view.params as any).isUnbounded;
@@ -274,6 +288,13 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                     isRelative: isRelativeParam,
                     isUnbounded: isUnboundedParam,
                 }, { description: 'Box around focused element.' }),
+                'camera-target': PD.Group({
+                    radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
+                    detailLevel: { ...detailLevel, isHidden: true },
+                    dynamicDetailLevel: dynamicDetailLevel,
+                    isRelative: isRelativeParam,
+                    isUnbounded: isUnboundedParam,
+                }, { description: 'Box around camera target.' }),
                 'cell': PD.Group({
                     detailLevel,
                     isRelative: isRelativeParam,
@@ -282,12 +303,11 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                 'auto': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
                     detailLevel,
-                    selectionDetailLevel: { ...detailLevel, label: 'Selection Detail' },
+                    selectionDetailLevel: selectionDetailLevel,
                     isRelative: isRelativeParam,
                     isUnbounded: isUnboundedParam,
                 }, { description: 'Box around focused element.' }),
-                // 'auto': PD.Group({  }), // TODO based on camera distance/active selection/whatever, show whole structure or slice.
-            }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Whole Structure" shows the volume for the whole structure.' })
+            }, { options: VolumeStreaming.ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Focus" shows the volume around the element/atom last interacted with. "Around Camera" shows the volume around the point the camera is targeting. "Whole Structure" shows the volume for the whole structure.' })
         };
         const options = {
             entry: params.entry.name,
@@ -299,6 +319,7 @@ export class VolumeStreamingCustomControls extends PluginUIComponent<StateTransf
                     bottomLeft: (params.entry.params.view.params as any).bottomLeft,
                     topRight: (params.entry.params.view.params as any).topRight,
                     selectionDetailLevel: (params.entry.params.view.params as any).selectionDetailLevel,
+                    dynamicDetailLevel: (params.entry.params.view.params as any).dynamicDetailLevel,
                     isRelative,
                     isUnbounded
                 }
diff --git a/src/mol-plugin-ui/index.ts b/src/mol-plugin-ui/index.ts
index 4b32f50fa698c9a20535fc663adb60d194cd0f8e..01c4f85c9f6c56766a511703e39a23de94f09fd3 100644
--- a/src/mol-plugin-ui/index.ts
+++ b/src/mol-plugin-ui/index.ts
@@ -18,5 +18,10 @@ export async function createPluginUI(target: HTMLElement, spec?: PluginUISpec, o
         await options.onBeforeUIRender(ctx);
     }
     ReactDOM.render(React.createElement(Plugin, { plugin: ctx }), target);
+    try {
+        await ctx.canvas3dInitialized;
+    } catch {
+        // Error reported in UI/console elsewhere.
+    }
     return ctx;
 }
\ No newline at end of file
diff --git a/src/mol-plugin-ui/left-panel.tsx b/src/mol-plugin-ui/left-panel.tsx
index 32477e580739e3388dbf0763db542fb97eb3c09c..62db9be7c613c82d0d1e8974d3523437aca376cf 100644
--- a/src/mol-plugin-ui/left-panel.tsx
+++ b/src/mol-plugin-ui/left-panel.tsx
@@ -141,11 +141,13 @@ class FullSettings extends PluginUIComponent {
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
         this.subscribe(this.plugin.layout.events.updated, () => this.forceUpdate());
 
-        this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
-            if (state.radiusMax !== undefined || state.radius !== undefined) {
-                this.forceUpdate();
-            }
-        });
+        if (this.plugin.canvas3d) {
+            this.subscribe(this.plugin.canvas3d.camera.stateChanged, state => {
+                if (state.radiusMax !== undefined || state.radius !== undefined) {
+                    this.forceUpdate();
+                }
+            });
+        }
     }
 
     render() {
diff --git a/src/mol-plugin-ui/plugin.tsx b/src/mol-plugin-ui/plugin.tsx
index a6b4da446e27ff4e7b15409d9c07d288cee5a568..fc492fe8caa0a02f8307886137cd4d1ce88cd983 100644
--- a/src/mol-plugin-ui/plugin.tsx
+++ b/src/mol-plugin-ui/plugin.tsx
@@ -24,14 +24,6 @@ import { BehaviorSubject } from 'rxjs';
 import { useBehavior } from './hooks/use-behavior';
 
 export class Plugin extends React.Component<{ plugin: PluginUIContext, children?: any }, {}> {
-    region(kind: 'left' | 'right' | 'bottom' | 'main', element: JSX.Element) {
-        return <div className={`msp-layout-region msp-layout-${kind}`}>
-            <div className='msp-layout-static'>
-                {element}
-            </div>
-        </div>;
-    }
-
     render() {
         return <PluginReactContext.Provider value={this.props.plugin}>
             <Layout />
diff --git a/src/mol-plugin-ui/react18.ts b/src/mol-plugin-ui/react18.ts
index 6309eebc1a7f0bd49aed99ac13662ac0a8a02bb4..fd9113ba83a1257c533bbbf7e3fb5d8f52aae94f 100644
--- a/src/mol-plugin-ui/react18.ts
+++ b/src/mol-plugin-ui/react18.ts
@@ -18,5 +18,10 @@ export async function createPluginUI(target: HTMLElement, spec?: PluginUISpec, o
         await options.onBeforeUIRender(ctx);
     }
     createRoot(target).render(createElement(Plugin, { plugin: ctx }));
+    try {
+        await ctx.canvas3dInitialized;
+    } catch {
+        // Error reported in UI/console elsewhere.
+    }
     return ctx;
 }
\ No newline at end of file
diff --git a/src/mol-plugin-ui/structure/components.tsx b/src/mol-plugin-ui/structure/components.tsx
index 797286a746d2ec8ae5d86c44ef49a12e380f43d2..0454a5772864732cb86e42c7ee48a5380e2a60c6 100644
--- a/src/mol-plugin-ui/structure/components.tsx
+++ b/src/mol-plugin-ui/structure/components.tsx
@@ -280,7 +280,6 @@ class StructureComponentGroup extends PurePluginUIComponent<{ group: StructureCo
 
     selectAction: ActionMenu.OnSelect = item => {
         if (!item) return;
-        this.setState({ action: void 0 });
         (item?.value as any)();
     };
 
diff --git a/src/mol-plugin-ui/structure/measurements.tsx b/src/mol-plugin-ui/structure/measurements.tsx
index cd26069728850817262781ff669b27c229f9c573..f60269f9056abd696e2f4378d0f4313539f1966e 100644
--- a/src/mol-plugin-ui/structure/measurements.tsx
+++ b/src/mol-plugin-ui/structure/measurements.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 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>
@@ -288,7 +288,12 @@ class MeasurementEntry extends PurePluginUIComponent<{ cell: StructureMeasuremen
         for (const loci of this.lociArray) {
             this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
         }
-        this.plugin.managers.interactivity.lociHighlights.highlight({ loci: this.props.cell.obj?.data.repr.getLoci()! }, false);
+        const reprLocis = this.props.cell.obj?.data.repr.getAllLoci();
+        if (reprLocis) {
+            for (const loci of reprLocis) {
+                this.plugin.managers.interactivity.lociHighlights.highlight({ loci }, false);
+            }
+        }
     };
 
     clearHighlight = () => {
diff --git a/src/mol-plugin-ui/viewport/canvas.tsx b/src/mol-plugin-ui/viewport/canvas.tsx
index 616c145d1bb5033e6ae956f125fe780863ff6f21..c75331955ad7f77ba1c285d5d2edbac84708c2b1 100644
--- a/src/mol-plugin-ui/viewport/canvas.tsx
+++ b/src/mol-plugin-ui/viewport/canvas.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -19,13 +19,14 @@ export interface ViewportCanvasParams {
 
     parentClassName?: string,
     parentStyle?: React.CSSProperties,
+    // NOTE: hostClassName/hostStyle no longer in use
+    // TODO: remove in 4.0
     hostClassName?: string,
     hostStyle?: React.CSSProperties,
 }
 
 export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, ViewportCanvasState> {
     private container = React.createRef<HTMLDivElement>();
-    private canvas = React.createRef<HTMLCanvasElement>();
 
     state: ViewportCanvasState = {
         noWebGl: false,
@@ -37,7 +38,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
     };
 
     componentDidMount() {
-        if (!this.canvas.current || !this.container.current || !this.plugin.initViewer(this.canvas.current!, this.container.current!)) {
+        if (!this.container.current || !this.plugin.mount(this.container.current!, { checkeredCanvasBackground: true })) {
             this.setState({ noWebGl: true });
             return;
         }
@@ -47,7 +48,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
 
     componentWillUnmount() {
         super.componentWillUnmount();
-        // TODO viewer cleanup
+        this.plugin.unmount();
     }
 
     renderMissing() {
@@ -59,7 +60,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
         return <div className='msp-no-webgl'>
             <div>
                 <p><b>WebGL does not seem to be available.</b></p>
-                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps.</p>
+                <p>This can be caused by an outdated browser, graphics card driver issue, or bad weather. Sometimes, just restarting the browser helps. Also, make sure hardware acceleration is enabled in your browser.</p>
                 <p>For a list of supported browsers, refer to <a href='http://caniuse.com/#feat=webgl' target='_blank'>http://caniuse.com/#feat=webgl</a>.</p>
             </div>
         </div>;
@@ -70,10 +71,7 @@ export class ViewportCanvas extends PluginUIComponent<ViewportCanvasParams, View
 
         const Logo = this.props.logo;
 
-        return <div className={this.props.parentClassName || 'msp-viewport'} style={this.props.parentStyle}>
-            <div className={this.props.hostClassName || 'msp-viewport-host3d'} style={this.props.hostStyle} ref={this.container}>
-                <canvas ref={this.canvas} />
-            </div>
+        return <div className={this.props.parentClassName || 'msp-viewport'} style={this.props.parentStyle} ref={this.container}>
             {(this.state.showLogo && Logo) && <Logo />}
         </div>;
     }
diff --git a/src/mol-plugin-ui/viewport/help.tsx b/src/mol-plugin-ui/viewport/help.tsx
index 468e63959c0d35cb68ddc733079f6b1306f451b2..9146fb6a7e461ccfc2d9be0f3b1ee310723b3c53 100644
--- a/src/mol-plugin-ui/viewport/help.tsx
+++ b/src/mol-plugin-ui/viewport/help.tsx
@@ -7,14 +7,15 @@
 import * as React from 'react';
 import { Binding } from '../../mol-util/binding';
 import { PluginUIComponent } from '../base';
-import { StateTransformer, StateSelection } from '../../mol-state';
+import { StateTransformer, StateSelection, State } from '../../mol-state';
 import { SelectLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { FocusLoci } from '../../mol-plugin/behavior/dynamic/representation';
 import { Icon, ArrowDropDownSvg, ArrowRightSvg, CameraSvg } from '../controls/icons';
 import { Button } from '../controls/common';
+import { memoizeLatest } from '../../mol-util/memoize';
 
 function getBindingsList(bindings: { [k: string]: Binding }) {
-    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]);
+    return Object.keys(bindings).map(k => [k, bindings[k]] as [string, Binding]).filter(b => Binding.isBinding(b[1]));
 }
 
 export class BindingsHelp extends React.PureComponent<{ bindings: { [k: string]: Binding } }> {
@@ -77,19 +78,30 @@ export class ViewportHelpContent extends PluginUIComponent<{ selectOnly?: boolea
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
     }
 
-    render() {
-        const interactionBindings: { [k: string]: Binding } = {};
-        this.plugin.spec.behaviors.forEach(b => {
-            const { bindings } = b.defaultParams;
-            if (bindings) Object.assign(interactionBindings, bindings);
+    getInteractionBindings = memoizeLatest((cells: State.Cells) => {
+        let interactionBindings: { [k: string]: Binding } | undefined = void 0;
+
+        cells.forEach(c => {
+            const params = c.params?.values;
+            if (params?.bindings && Object.keys(params.bindings).length > 0) {
+                if (!interactionBindings) interactionBindings = { };
+                Object.assign(interactionBindings, params.bindings);
+            }
         });
+
+        return interactionBindings;
+    });
+
+    render() {
+        const interactionBindings = this.getInteractionBindings(this.plugin.state.behaviors.cells);
+
         return <>
             {(!this.props.selectOnly && this.plugin.canvas3d) && <HelpGroup key='trackball' header='Moving in 3D'>
                 <BindingsHelp bindings={this.plugin.canvas3d.props.trackball.bindings} />
             </HelpGroup>}
-            <HelpGroup key='interactions' header='Mouse Controls'>
+            {!!interactionBindings && <HelpGroup key='interactions' header='Mouse Controls'>
                 <BindingsHelp bindings={interactionBindings} />
-            </HelpGroup>
+            </HelpGroup>}
         </>;
     }
 }
diff --git a/src/mol-plugin-ui/viewport/screenshot.tsx b/src/mol-plugin-ui/viewport/screenshot.tsx
index 135beee19ae88e1204d2cda6eaa926e91b65eb8d..9158ef2b58961341f817a5bca89c5b663f28c477 100644
--- a/src/mol-plugin-ui/viewport/screenshot.tsx
+++ b/src/mol-plugin-ui/viewport/screenshot.tsx
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 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>
@@ -96,18 +96,21 @@ export class DownloadScreenshotControls extends PluginUIComponent<{ close: () =>
 }
 
 function ScreenshotParams({ plugin, isDisabled }: { plugin: PluginContext, isDisabled: boolean }) {
-    const helper = plugin.helpers.viewportScreenshot!;
-    const values = useBehavior(helper.behaviors.values);
+    const helper = plugin.helpers.viewportScreenshot;
+
+    const values = useBehavior(helper?.behaviors.values);
+    if (!helper) return null;
 
     return <ParameterControls params={helper.params} values={values} onChangeValues={v => helper.behaviors.values.next(v)} isDisabled={isDisabled} />;
 }
 
 function CropControls({ plugin }: { plugin: PluginContext }) {
     const helper = plugin.helpers.viewportScreenshot;
-    const cropParams = useBehavior(helper?.behaviors.cropParams!);
+
+    const cropParams = useBehavior(helper?.behaviors.cropParams);
     useBehavior(helper?.behaviors.relativeCrop);
 
-    if (!helper) return null;
+    if (!helper || !cropParams) return null;
 
     return <div style={{ width: '100%', height: '24px', marginTop: '8px' }}>
         <ToggleButton icon={CropOrginalSvg} title='Auto-crop' inline isSelected={cropParams.auto}
diff --git a/src/mol-plugin-ui/viewport/simple-settings.tsx b/src/mol-plugin-ui/viewport/simple-settings.tsx
index 73f819331fbc1188e0d594da430e1fb3ec6af0be..a3f7eb6d939a26fb0db47839521823779989c94f 100644
--- a/src/mol-plugin-ui/viewport/simple-settings.tsx
+++ b/src/mol-plugin-ui/viewport/simple-settings.tsx
@@ -8,8 +8,10 @@
 import { produce } from 'immer';
 import { Canvas3DParams, Canvas3DProps } from '../../mol-canvas3d/canvas3d';
 import { PluginCommands } from '../../mol-plugin/commands';
+import { PluginConfig } from '../../mol-plugin/config';
 import { StateTransform } from '../../mol-state';
 import { Color } from '../../mol-util/color';
+import { deepClone } from '../../mol-util/object';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
 import { ParamMapping } from '../../mol-util/param-mapping';
 import { Mutable } from '../../mol-util/type-helpers';
@@ -20,6 +22,8 @@ import { ViewportHelpContent } from './help';
 
 export class SimpleSettingsControl extends PluginUIComponent {
     componentDidMount() {
+        if (!this.plugin.canvas3d) return;
+
         this.subscribe(this.plugin.events.canvas3d.settingsUpdated, () => this.forceUpdate());
 
         this.subscribe(this.plugin.canvas3d!.camera.stateChanged, state => {
@@ -50,7 +54,8 @@ const SimpleSettingsParams = {
     camera: Canvas3DParams.camera,
     background: PD.Group({
         color: PD.Color(Color(0xFCFBF9), { label: 'Background', description: 'Custom background color' }),
-        transparent: PD.Boolean(false)
+        transparent: PD.Boolean(false),
+        style: Canvas3DParams.postprocessing.params.background,
     }, { pivot: 'color' }),
     lighting: PD.Group({
         occlusion: Canvas3DParams.postprocessing.params.occlusion,
@@ -75,6 +80,13 @@ const SimpleSettingsMapping = ParamMapping({
             if (controls.left !== 'none') options.push(['left', LayoutOptions.left]);
             params.layout.options = options;
         }
+        const bgStyles = ctx.config.get(PluginConfig.Background.Styles) || [];
+        if (bgStyles.length > 0) {
+            Object.assign(params.background.params.style, {
+                presets: deepClone(bgStyles),
+                isFlat: false, // so the presets menu is shown
+            });
+        }
         return params;
     },
     target(ctx: PluginUIContext) {
@@ -97,7 +109,8 @@ const SimpleSettingsMapping = ParamMapping({
             camera: canvas.camera,
             background: {
                 color: renderer.backgroundColor,
-                transparent: canvas.transparentBackground
+                transparent: canvas.transparentBackground,
+                style: canvas.postprocessing.background,
             },
             lighting: {
                 occlusion: canvas.postprocessing.occlusion,
@@ -117,10 +130,12 @@ const SimpleSettingsMapping = ParamMapping({
         canvas.renderer.backgroundColor = s.background.color;
         canvas.postprocessing.occlusion = s.lighting.occlusion;
         canvas.postprocessing.outline = s.lighting.outline;
+        canvas.postprocessing.background = s.background.style;
         canvas.cameraFog = s.lighting.fog;
         canvas.cameraClipping = {
             radius: s.clipping.radius,
             far: s.clipping.far,
+            minNear: s.clipping.minNear,
         };
 
         props.layout = s.layout;
diff --git a/src/mol-plugin/behavior/behavior.ts b/src/mol-plugin/behavior/behavior.ts
index 86b809fb40e0742a052977e9d4b6abf3f25e7521..96ada468ba35d0960ae2481f20545c2884ab229d 100644
--- a/src/mol-plugin/behavior/behavior.ts
+++ b/src/mol-plugin/behavior/behavior.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Adam Midlik <midlik@gmail.com>
  */
 
 import { PluginStateTransform, PluginStateObject } from '../../mol-plugin-state/objects';
@@ -17,7 +18,8 @@ export { PluginBehavior };
 
 interface PluginBehavior<P = unknown> {
     register(ref: StateTransform.Ref): void,
-    unregister(): void,
+    unregister?(): void,
+    dispose?(): void,
 
     /** Update params in place. Optionally return a promise if it depends on an async action. */
     update?(params: P): boolean | Promise<boolean>
@@ -38,7 +40,7 @@ namespace PluginBehavior {
         'misc': 'Miscellaneous'
     };
 
-    export interface CreateParams<P> {
+    export interface CreateParams<P extends {}> {
         name: string,
         category: keyof typeof Categories,
         ctor: Ctor<P>,
@@ -71,7 +73,7 @@ namespace PluginBehavior {
         return categoryMap.get(t.id)!;
     }
 
-    export function create<P>(params: CreateParams<P>) {
+    export function create<P extends {}>(params: CreateParams<P>) {
         const t = PluginStateTransform.CreateBuiltIn<Category, Behavior, P>({
             name: params.name,
             display: params.display,
@@ -102,7 +104,7 @@ namespace PluginBehavior {
             register(): void {
                 this.sub = cmd.subscribe(this.ctx, data => action(data, this.ctx));
             }
-            unregister(): void {
+            dispose(): void {
                 if (this.sub) this.sub.unsubscribe();
                 this.sub = void 0;
             }
@@ -111,7 +113,7 @@ namespace PluginBehavior {
         };
     }
 
-    export abstract class Handler<P = { }> implements PluginBehavior<P> {
+    export abstract class Handler<P extends {} = {}> implements PluginBehavior<P> {
         private subs: PluginCommand.Subscription[] = [];
         protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
             this.subs.push(cmd.subscribe(this.ctx, action));
@@ -123,7 +125,7 @@ namespace PluginBehavior {
             this.subs.push(sub);
         }
         abstract register(): void;
-        unregister() {
+        dispose(): void {
             for (const s of this.subs) s.unsubscribe();
             this.subs = [];
         }
@@ -143,11 +145,20 @@ namespace PluginBehavior {
         protected subscribeCommand<T>(cmd: PluginCommand<T>, action: PluginCommand.Action<T>) {
             this.subs.push(cmd.subscribe(this.plugin, action));
         }
-        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void) {
-            this.subs.push(o.subscribe(action));
+        protected subscribeObservable<T>(o: Observable<T>, action: (v: T) => void): PluginCommand.Subscription {
+            const sub = o.subscribe(action);
+            this.subs.push(sub);
+            return {
+                unsubscribe: () => {
+                    const idx = this.subs.indexOf(sub);
+                    if (idx >= 0) {
+                        this.subs.splice(idx, 1);
+                        sub.unsubscribe();
+                    }
+                }
+            };
         }
-
-        unregister() {
+        dispose(): void {
             for (const s of this.subs) s.unsubscribe();
             this.subs = [];
         }
diff --git a/src/mol-plugin/behavior/dynamic/custom-props/structure-info.ts b/src/mol-plugin/behavior/dynamic/custom-props/structure-info.ts
index 469fca3463ec3e26164cb77ffec055deb2c0510e..e2484b9ce1c95f8cb62b071636425f4ffa9ed1f1 100644
--- a/src/mol-plugin/behavior/dynamic/custom-props/structure-info.ts
+++ b/src/mol-plugin/behavior/dynamic/custom-props/structure-info.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -52,17 +52,43 @@ export const StructureInfo = PluginBehavior.create({
             return { auth, label };
         }
 
+        private setModelMaxIndex() {
+            const value = this.maxModelIndex;
+            const cells = this.ctx.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Model));
+            for (const c of cells) {
+                const m = c.obj?.data;
+                if (m) {
+                    if (Model.MaxIndex.get(m).value !== value) {
+                        Model.MaxIndex.set(m, { value }, value);
+                    }
+                }
+            }
+        }
+
+        private setStructureMaxIndex() {
+            const value = this.maxModelIndex;
+            const cells = this.ctx.state.data.select(StateSelection.Generators.rootsOfType(PluginStateObject.Molecule.Structure));
+            for (const c of cells) {
+                const s = c.obj?.data;
+                if (s) {
+                    if (Structure.MaxIndex.get(s).value !== value) {
+                        Structure.MaxIndex.set(s, { value }, value);
+                    }
+                }
+            }
+        }
+
         private handleModel(model: Model, oldModel?: Model) {
             if (Model.Index.get(model).value === undefined) {
                 const oldIndex = oldModel && Model.Index.get(oldModel).value;
                 const value = oldIndex ?? (this.maxModelIndex + 1);
-                Model.Index.set(model, { value });
+                Model.Index.set(model, { value }, value);
             }
 
             if (Model.AsymIdOffset.get(model).value === undefined) {
                 const oldOffset = oldModel && Model.AsymIdOffset.get(oldModel).value;
                 const value = oldOffset ?? { ...this.asymIdOffset };
-                Model.AsymIdOffset.set(model, { value });
+                Model.AsymIdOffset.set(model, { value }, value);
             }
         }
 
@@ -72,7 +98,7 @@ export const StructureInfo = PluginBehavior.create({
 
             const oldIndex = oldStructure && Structure.Index.get(oldStructure).value;
             const value = oldIndex ?? (this.maxStructureIndex + 1);
-            Structure.Index.set(structure, { value });
+            Structure.Index.set(structure, { value }, value);
         }
 
         private handle(ref: string, obj: StateObject<any, StateObject.Type<any>>, oldObj?: StateObject<any, StateObject.Type<any>>) {
@@ -92,10 +118,14 @@ export const StructureInfo = PluginBehavior.create({
         register(): void {
             this.ctx.customModelProperties.register(Model.AsymIdOffset, true);
             this.ctx.customModelProperties.register(Model.Index, true);
+            this.ctx.customModelProperties.register(Model.MaxIndex, true);
             this.ctx.customStructureProperties.register(Structure.Index, true);
+            this.ctx.customStructureProperties.register(Structure.MaxIndex, true);
 
             this.subscribeObservable(this.ctx.state.data.events.object.created, o => {
                 this.handle(o.ref, o.obj);
+                this.setModelMaxIndex();
+                this.setStructureMaxIndex();
             });
 
             this.subscribeObservable(this.ctx.state.data.events.object.updated, o => {
@@ -106,7 +136,9 @@ export const StructureInfo = PluginBehavior.create({
         unregister() {
             this.ctx.customModelProperties.unregister(Model.AsymIdOffset.descriptor.name);
             this.ctx.customModelProperties.unregister(Model.Index.descriptor.name);
+            this.ctx.customModelProperties.unregister(Model.MaxIndex.descriptor.name);
             this.ctx.customStructureProperties.unregister(Structure.Index.descriptor.name);
+            this.ctx.customStructureProperties.unregister(Structure.MaxIndex.descriptor.name);
         }
     }
 });
\ No newline at end of file
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
index 071e4544ac1f234044caa0d222ad3953025c44bf..1050783faf4dac3ea5437eb5c619f754c2f97dd3 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/behavior.ts
@@ -1,8 +1,9 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Adam Midlik <midlik@gmail.com>
  */
 
 import { ParamDefinition as PD } from '../../../../mol-util/param-definition';
@@ -24,6 +25,10 @@ import { PluginContext } from '../../../context';
 import { EmptyLoci, Loci, isEmptyLoci } from '../../../../mol-model/loci';
 import { Asset } from '../../../../mol-util/assets';
 import { GlobalModelTransformInfo } from '../../../../mol-model/structure/model/properties/global-transform';
+import { distinctUntilChanged, filter, map, Observable, throttleTime } from 'rxjs';
+import { Camera } from '../../../../mol-canvas3d/camera';
+import { PluginCommand } from '../../../command';
+import { SingleAsyncQueue } from '../../../../mol-util/single-async-queue';
 
 export class VolumeStreaming extends PluginStateObject.CreateBehavior<VolumeStreaming.Behavior>({ name: 'Volume Streaming' }) { }
 
@@ -53,7 +58,7 @@ export namespace VolumeStreaming {
         valuesInfo: [{ mean: 0, min: -1, max: 1, sigma: 0.1 }, { mean: 0, min: -1, max: 1, sigma: 0.1 }]
     };
 
-    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = { }) {
+    export function createParams(options: { data?: VolumeServerInfo.Data, defaultView?: ViewTypes, channelParams?: DefaultChannelParams } = {}) {
         const { data, defaultView, channelParams } = options;
         const map = new Map<string, VolumeServerInfo.EntryData>();
         if (data) data.entries.forEach(d => map.set(d.dataId, d));
@@ -68,14 +73,14 @@ export namespace VolumeStreaming {
     export type EntryParams = PD.Values<EntryParamDefinition>
 
     export function createEntryParams(options: { entryData?: VolumeServerInfo.EntryData, defaultView?: ViewTypes, structure?: Structure, channelParams?: DefaultChannelParams }) {
-        const { entryData, defaultView, structure, channelParams = { } } = options;
+        const { entryData, defaultView, structure, channelParams = {} } = options;
 
         // fake the info
         const info = entryData || { kind: 'em', header: { sampling: [fakeSampling], availablePrecisions: [{ precision: 0, maxVoxels: 0 }] }, emDefaultContourLevel: Volume.IsoValue.relative(0) };
         const box = (structure && structure.boundary.box) || Box3D();
 
         return {
-            view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'cell' : 'selection-box'), {
+            view: PD.MappedStatic(defaultView || (info.kind === 'em' ? 'auto' : 'selection-box'), {
                 'off': PD.Group<{}>({}),
                 'box': PD.Group({
                     bottomLeft: PD.Vec3(box.min),
@@ -86,19 +91,24 @@ export namespace VolumeStreaming {
                     bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                     topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
                 }, { description: 'Box around focused element.', isFlat: true }),
+                'camera-target': PD.Group({
+                    radius: PD.Numeric(0.5, { min: 0, max: 1, step: 0.05 }, { description: 'Radius within which the volume is shown (relative to the field of view).' }),
+                    // Minimal detail level for the inside of the zoomed region (real detail can be higher, depending on the region size)
+                    dynamicDetailLevel: createDetailParams(info.header.availablePrecisions, 0, { label: 'Dynamic Detail' }),
+                    bottomLeft: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                    topRight: PD.Vec3(Vec3.create(0, 0, 0), {}, { isHidden: true }),
+                }, { description: 'Box around camera target.', isFlat: true }),
                 'cell': PD.Group<{}>({}),
                 // Show selection-box if available and cell otherwise.
                 'auto': PD.Group({
                     radius: PD.Numeric(5, { min: 0, max: 50, step: 0.5 }, { description: 'Radius in \u212B within which the volume is shown.' }),
-                    selectionDetailLevel: PD.Select<number>(Math.min(6, info.header.availablePrecisions.length - 1),
-                        info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { label: 'Selection Detail', description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
+                    selectionDetailLevel: createDetailParams(info.header.availablePrecisions, 6, { label: 'Selection Detail' }),
                     isSelection: PD.Boolean(false, { isHidden: true }),
                     bottomLeft: PD.Vec3(box.min, {}, { isHidden: true }),
                     topRight: PD.Vec3(box.max, {}, { isHidden: true }),
                 }, { description: 'Box around focused element.', isFlat: true })
             }, { options: ViewTypeOptions, description: 'Controls what of the volume is displayed. "Off" hides the volume alltogether. "Bounded box" shows the volume inside the given box. "Around Interaction" shows the volume around the focused element/atom. "Whole Structure" shows the volume for the whole structure.' }),
-            detailLevel: PD.Select<number>(Math.min(3, info.header.availablePrecisions.length - 1),
-                info.header.availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]), { description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 0 (0.52M voxels) to 6 (25.17M voxels).' }),
+            detailLevel: createDetailParams(info.header.availablePrecisions, 3),
             channels: info.kind === 'em'
                 ? PD.Group({
                     'em': channelParam('EM', Color(0x638F8F), info.emDefaultContourLevel || Volume.IsoValue.relative(1), info.header.sampling[0].valuesInfo[0], channelParams['em'])
@@ -111,13 +121,40 @@ export namespace VolumeStreaming {
         };
     }
 
-    export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
+    function createDetailParams(availablePrecisions: VolumeServerHeader.DetailLevel[], preferredPrecision: number, info?: PD.Info) {
+        return PD.Select<number>(Math.min(preferredPrecision, availablePrecisions.length - 1),
+            availablePrecisions.map((p, i) => [i, `${i + 1} [ ${Math.pow(p.maxVoxels, 1 / 3) | 0}^3 cells ]`] as [number, string]),
+            {
+                description: 'Determines the maximum number of voxels. Depending on the size of the volume options are in the range from 1 (0.52M voxels) to 7 (25.17M voxels).',
+                ...info
+            }
+        );
+    }
 
-    export type ViewTypes = 'off' | 'box' | 'selection-box' | 'cell' | 'auto'
+    export function copyParams(origParams: Params): Params {
+        return {
+            entry: {
+                name: origParams.entry.name,
+                params: {
+                    detailLevel: origParams.entry.params.detailLevel,
+                    channels: origParams.entry.params.channels,
+                    view: {
+                        name: origParams.entry.params.view.name,
+                        params: { ...origParams.entry.params.view.params } as any,
+                    }
+                }
+            }
+        };
+    }
+
+    export const ViewTypeOptions = [['off', 'Off'], ['box', 'Bounded Box'], ['selection-box', 'Around Focus'], ['camera-target', 'Around Camera'], ['cell', 'Whole Structure'], ['auto', 'Auto']] as [ViewTypes, string][];
+
+    export type ViewTypes = 'off' | 'box' | 'selection-box' | 'camera-target' | 'cell' | 'auto'
 
     export type ParamDefinition = ReturnType<typeof createParams>
     export type Params = PD.Values<ParamDefinition>
 
+
     type ChannelsInfo = { [name in ChannelType]?: { isoValue: Volume.IsoValue, color: Color, wireframe: boolean, opacity: number } }
     type ChannelsData = { [name in 'EM' | '2FO-FC' | 'FO-FC']?: Volume }
 
@@ -140,6 +177,14 @@ export namespace VolumeStreaming {
         private lastLoci: StructureElement.Loci | EmptyLoci = EmptyLoci;
         private ref: string = '';
         public infoMap: Map<string, VolumeServerInfo.EntryData>;
+        private updateQueue: SingleAsyncQueue;
+        private cameraTargetObservable = this.plugin.canvas3d!.didDraw!.pipe(
+            throttleTime(500, undefined, { 'leading': true, 'trailing': true }),
+            map(() => this.plugin.canvas3d?.camera.getSnapshot()),
+            distinctUntilChanged((a, b) => this.isCameraTargetSame(a, b)),
+            filter(a => a !== undefined),
+        ) as Observable<Camera.Snapshot>;
+        private cameraTargetSubscription?: PluginCommand.Subscription = undefined;
 
         channels: Channels = {};
 
@@ -163,6 +208,9 @@ export namespace VolumeStreaming {
             if (this.params.entry.params.view.name === 'auto' && this.params.entry.params.view.params.isSelection) {
                 detail = this.params.entry.params.view.params.selectionDetailLevel;
             }
+            if (this.params.entry.params.view.name === 'camera-target' && box) {
+                detail = this.decideDetail(box, this.params.entry.params.view.params.dynamicDetailLevel);
+            }
 
             url += `?detail=${detail}`;
 
@@ -201,58 +249,21 @@ export namespace VolumeStreaming {
             return ret;
         }
 
-        private updateSelectionBoxParams(box: Box3D) {
-            if (this.params.entry.params.view.name !== 'selection-box') return;
-
-            const state = this.plugin.state.data;
-            const newParams: Params = {
-                ...this.params,
-                entry: {
-                    name: this.params.entry.name,
-                    params: {
-                        ...this.params.entry.params,
-                        view: {
-                            name: 'selection-box' as const,
-                            params: {
-                                radius: this.params.entry.params.view.params.radius,
-                                bottomLeft: box.min,
-                                topRight: box.max
-                            }
-                        }
-                    }
-                }
-            };
-            const update = state.build().to(this.ref).update(newParams);
-
-            PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
-        }
-
-        private updateAutoParams(box: Box3D | undefined, isSelection: boolean) {
-            if (this.params.entry.params.view.name !== 'auto') return;
+        private async updateParams(box: Box3D | undefined, autoIsSelection: boolean = false) {
+            const newParams = copyParams(this.params);
+            const viewType = newParams.entry.params.view.name;
+            if (viewType !== 'off' && viewType !== 'cell') {
+                newParams.entry.params.view.params.bottomLeft = box?.min || Vec3.zero();
+                newParams.entry.params.view.params.topRight = box?.max || Vec3.zero();
+            }
+            if (viewType === 'auto') {
+                newParams.entry.params.view.params.isSelection = autoIsSelection;
+            }
 
             const state = this.plugin.state.data;
-            const newParams: Params = {
-                ...this.params,
-                entry: {
-                    name: this.params.entry.name,
-                    params: {
-                        ...this.params.entry.params,
-                        view: {
-                            name: 'auto' as const,
-                            params: {
-                                radius: this.params.entry.params.view.params.radius,
-                                selectionDetailLevel: this.params.entry.params.view.params.selectionDetailLevel,
-                                isSelection,
-                                bottomLeft: box?.min || Vec3.zero(),
-                                topRight: box?.max || Vec3.zero()
-                            }
-                        }
-                    }
-                }
-            };
             const update = state.build().to(this.ref).update(newParams);
 
-            PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
+            await PluginCommands.State.Update(this.plugin, { state, tree: update, options: { doNotUpdateCurrent: true } });
         }
 
         private getStructureRoot() {
@@ -303,6 +314,18 @@ export namespace VolumeStreaming {
             }
         }
 
+        private isCameraTargetSame(a?: Camera.Snapshot, b?: Camera.Snapshot): boolean {
+            if (!a || !b) return false;
+            const targetSame = Vec3.equals(a.target, b.target);
+            const sqDistA = Vec3.squaredDistance(a.target, a.position);
+            const sqDistB = Vec3.squaredDistance(b.target, b.position);
+            const distanceSame = Math.abs(sqDistA - sqDistB) / sqDistA < 1e-3;
+            return targetSame && distanceSame;
+        }
+        private cameraTargetDistance(snapshot: Camera.Snapshot): number {
+            return Vec3.distance(snapshot.target, snapshot.position);
+        }
+
         private _invTransform: Mat4 = Mat4();
         private getBoxFromLoci(loci: StructureElement.Loci | EmptyLoci): Box3D {
             if (Loci.isEmpty(loci) || isEmptyLoci(loci)) {
@@ -328,39 +351,82 @@ export namespace VolumeStreaming {
         }
 
         private updateAuto(loci: StructureElement.Loci | EmptyLoci) {
-            // if (Loci.areEqual(this.lastLoci, loci)) {
-            //     this.lastLoci = EmptyLoci;
-            //     this.updateSelectionBoxParams(Box3D.empty());
-            //     return;
-            // }
-
-            this.lastLoci = loci;
-
-            if (isEmptyLoci(loci)) {
-                this.updateAutoParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
-                return;
-            }
-
-            const box = this.getBoxFromLoci(loci);
-            this.updateAutoParams(box, true);
+            this.updateQueue.enqueue(async () => {
+                this.lastLoci = loci;
+                if (isEmptyLoci(loci)) {
+                    await this.updateParams(this.info.kind === 'x-ray' ? this.data.structure.boundary.box : void 0, false);
+                } else {
+                    await this.updateParams(this.getBoxFromLoci(loci), true);
+                }
+            });
         }
 
         private updateSelectionBox(loci: StructureElement.Loci | EmptyLoci) {
-            if (Loci.areEqual(this.lastLoci, loci)) {
-                this.lastLoci = EmptyLoci;
-                this.updateSelectionBoxParams(Box3D());
-                return;
-            }
+            this.updateQueue.enqueue(async () => {
+                if (Loci.areEqual(this.lastLoci, loci)) {
+                    this.lastLoci = EmptyLoci;
+                } else {
+                    this.lastLoci = loci;
+                }
+                const box = this.getBoxFromLoci(this.lastLoci);
+                await this.updateParams(box);
+            });
+        }
 
-            this.lastLoci = loci;
+        private updateCameraTarget(snapshot: Camera.Snapshot) {
+            this.updateQueue.enqueue(async () => {
+                const origManualReset = this.plugin.canvas3d?.props.camera.manualReset;
+                try {
+                    if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: true } });
+                    const box = this.boxFromCameraTarget(snapshot, true);
+                    await this.updateParams(box);
+                } finally {
+                    if (!origManualReset) this.plugin.canvas3d?.setProps({ camera: { manualReset: origManualReset } });
+                }
+            });
+        }
 
-            if (isEmptyLoci(loci)) {
-                this.updateSelectionBoxParams(Box3D());
-                return;
+        private boxFromCameraTarget(snapshot: Camera.Snapshot, boundByBoundarySize: boolean): Box3D {
+            const target = snapshot.target;
+            const distance = this.cameraTargetDistance(snapshot);
+            const top = Math.tan(0.5 * snapshot.fov) * distance;
+            let radius = top;
+            const viewport = this.plugin.canvas3d?.camera.viewport;
+            if (viewport && viewport.width > viewport.height) {
+                radius *= viewport.width / viewport.height;
+            }
+            const relativeRadius = this.params.entry.params.view.name === 'camera-target' ? this.params.entry.params.view.params.radius : 0.5;
+            radius *= relativeRadius;
+            let radiusX, radiusY, radiusZ;
+            if (boundByBoundarySize) {
+                const bBoxSize = Vec3.zero();
+                Box3D.size(bBoxSize, this.data.structure.boundary.box);
+                radiusX = Math.min(radius, 0.5 * bBoxSize[0]);
+                radiusY = Math.min(radius, 0.5 * bBoxSize[1]);
+                radiusZ = Math.min(radius, 0.5 * bBoxSize[2]);
+            } else {
+                radiusX = radiusY = radiusZ = radius;
             }
+            return Box3D.create(
+                Vec3.create(target[0] - radiusX, target[1] - radiusY, target[2] - radiusZ),
+                Vec3.create(target[0] + radiusX, target[1] + radiusY, target[2] + radiusZ)
+            );
+        }
 
-            const box = this.getBoxFromLoci(loci);
-            this.updateSelectionBoxParams(box);
+        private decideDetail(box: Box3D, baseDetail: number): number {
+            const cellVolume = this.info.kind === 'x-ray'
+                ? Box3D.volume(this.data.structure.boundary.box)
+                : this.info.header.spacegroup.size.reduce((a, b) => a * b, 1);
+            const boxVolume = Box3D.volume(box);
+            let ratio = boxVolume / cellVolume;
+            const maxDetail = this.info.header.availablePrecisions.length - 1;
+            let detail = baseDetail;
+            while (ratio <= 0.5 && detail < maxDetail) {
+                ratio *= 2;
+                detail += 1;
+            }
+            // console.log(`Decided dynamic detail: ${detail}, (base detail: ${baseDetail}, box/cell volume ratio: ${boxVolume / cellVolume})`);
+            return detail;
         }
 
         async update(params: Params) {
@@ -369,6 +435,11 @@ export namespace VolumeStreaming {
             this.params = params;
             let box: Box3D | undefined = void 0, emptyData = false;
 
+            if (params.entry.params.view.name !== 'camera-target' && this.cameraTargetSubscription) {
+                this.cameraTargetSubscription.unsubscribe();
+                this.cameraTargetSubscription = undefined;
+            }
+
             switch (params.entry.params.view.name) {
                 case 'off':
                     emptyData = true;
@@ -388,6 +459,12 @@ export namespace VolumeStreaming {
                     Box3D.expand(box, box, Vec3.create(r, r, r));
                     break;
                 }
+                case 'camera-target':
+                    if (!this.cameraTargetSubscription) {
+                        this.cameraTargetSubscription = this.subscribeObservable(this.cameraTargetObservable, (e) => this.updateCameraTarget(e));
+                    }
+                    box = this.boxFromCameraTarget(this.plugin.canvas3d!.camera.getSnapshot(), true);
+                    break;
                 case 'cell':
                     box = this.info.kind === 'x-ray'
                         ? this.data.structure.boundary.box
@@ -439,6 +516,7 @@ export namespace VolumeStreaming {
 
         getDescription() {
             if (this.params.entry.params.view.name === 'selection-box') return 'Selection';
+            if (this.params.entry.params.view.name === 'camera-target') return 'Camera';
             if (this.params.entry.params.view.name === 'box') return 'Static Box';
             if (this.params.entry.params.view.name === 'cell') return 'Cell';
             return '';
@@ -449,6 +527,7 @@ export namespace VolumeStreaming {
 
             this.infoMap = new Map<string, VolumeServerInfo.EntryData>();
             this.data.entries.forEach(info => this.infoMap.set(info.dataId, info));
+            this.updateQueue = new SingleAsyncQueue();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
index af1c49d98d59c0e2adf0d4ffd9c035f441866688..b58b00605022fd16192276152d8a8c5582cdd0ad 100644
--- a/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
+++ b/src/mol-plugin/behavior/dynamic/volume-streaming/transformers.ts
@@ -1,8 +1,9 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Adam Midlik <midlik@gmail.com>
  */
 
 import { PluginStateObject as SO, PluginStateTransform } from '../../../../mol-plugin-state/objects';
@@ -42,7 +43,7 @@ export const InitVolumeStreaming = StateAction.build({
         return {
             method: PD.Select<VolumeServerInfo.Kind>(method, [['em', 'EM'], ['x-ray', 'X-Ray']]),
             entries: PD.ObjectList({ id: PD.Text(ids[0] || '') }, ({ id }) => id, { defaultValue: ids.map(id => ({ id })) }),
-            defaultView: PD.Select<VolumeStreaming.ViewTypes>(method === 'em' ? 'cell' : 'selection-box', VolumeStreaming.ViewTypeOptions as any),
+            defaultView: PD.Select<VolumeStreaming.ViewTypes>(method === 'em' ? 'auto' : 'selection-box', VolumeStreaming.ViewTypeOptions as any),
             options: PD.Group({
                 serverUrl: PD.Text(plugin.config.get(PluginConfig.VolumeStreaming.DefaultServer) || 'https://ds.litemol.org'),
                 behaviorRef: PD.Text('', { isHidden: true }),
@@ -219,6 +220,7 @@ const CreateVolumeStreamingBehavior = PluginStateTransform.BuiltIn({
     canAutoUpdate: ({ oldParams, newParams }) => {
         return oldParams.entry.params.view === newParams.entry.params.view
             || newParams.entry.params.view.name === 'selection-box'
+            || newParams.entry.params.view.name === 'camera-target'
             || newParams.entry.params.view.name === 'off';
     },
     apply: ({ a, params }, plugin: PluginContext) => Task.create('Volume streaming', async _ => {
diff --git a/src/mol-plugin/behavior/static/state.ts b/src/mol-plugin/behavior/static/state.ts
index 989fe3337ffac3170c51a3c1cfbfa778debc8f56..e4270736da5816cc6b6b5ce7287a6878169b5b24 100644
--- a/src/mol-plugin/behavior/static/state.ts
+++ b/src/mol-plugin/behavior/static/state.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -37,12 +37,16 @@ export function SyncBehaviors(ctx: PluginContext) {
 
     ctx.state.events.object.removed.subscribe(o => {
         if (!SO.isBehavior(o.obj)) return;
-        o.obj.data.unregister();
+        o.obj.data.unregister?.();
+        o.obj.data.dispose?.();
     });
 
     ctx.state.events.object.updated.subscribe(o => {
         if (o.action === 'recreate') {
-            if (o.oldObj && SO.isBehavior(o.oldObj)) o.oldObj.data.unregister();
+            if (o.oldObj && SO.isBehavior(o.oldObj)) {
+                o.oldObj.data.unregister?.();
+                o.oldObj.data.dispose?.();
+            }
             if (o.obj && SO.isBehavior(o.obj)) o.obj.data.register(o.ref);
         }
     });
@@ -115,7 +119,9 @@ export function Highlight(ctx: PluginContext) {
                 ctx.managers.interactivity.lociHighlights.highlight({ loci: Structure.Loci(cell.obj.data) }, false);
             } else if (cell && SO.isRepresentation3D(cell.obj)) {
                 const { repr } = cell.obj.data;
-                ctx.managers.interactivity.lociHighlights.highlight({ loci: repr.getLoci(), repr }, false);
+                for (const loci of repr.getAllLoci()) {
+                    ctx.managers.interactivity.lociHighlights.highlight({ loci, repr }, false);
+                }
             } else if (SO.Molecule.Structure.Selections.is(cell.obj)) {
                 for (const entry of cell.obj.data) {
                     ctx.managers.interactivity.lociHighlights.highlight({ loci: entry.loci }, false);
diff --git a/src/mol-plugin/config.ts b/src/mol-plugin/config.ts
index b70305fb7f6773cc470aca8bc9733a15db101d87..8d097a20047244701bc05f203d6764ecc7ef274c 100644
--- a/src/mol-plugin/config.ts
+++ b/src/mol-plugin/config.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2020-2022 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>
@@ -12,6 +12,7 @@ import { EmdbDownloadProvider } from '../mol-plugin-state/actions/volume';
 import { StructureRepresentationPresetProvider } from '../mol-plugin-state/builder/structure/representation-preset';
 import { PluginFeatureDetection } from './features';
 import { SaccharideCompIdMapType } from '../mol-model/structure/structure/carbohydrates/constants';
+import { BackgroundProps } from '../mol-canvas3d/passes/background';
 
 export class PluginConfigItem<T = any> {
     toString() { return this.key; }
@@ -30,10 +31,12 @@ export const PluginConfig = {
         PixelScale: item('plugin-config.pixel-scale', 1),
         PickScale: item('plugin-config.pick-scale', 0.25),
         PickPadding: item('plugin-config.pick-padding', 3),
-        EnableWboit: item('plugin-config.enable-wboit', PluginFeatureDetection.wboit),
+        EnableWboit: item('plugin-config.enable-wboit', true),
+        EnableDpoit: item('plugin-config.enable-dpoit', false),
         // as of Oct 1 2021, WebGL 2 doesn't work on iOS 15.
         // TODO: check back in a few weeks to see if it was fixed
         PreferWebGl1: item('plugin-config.prefer-webgl1', PluginFeatureDetection.preferWebGl1),
+        AllowMajorPerformanceCaveat: item('plugin-config.allow-major-performance-caveat', false),
     },
     State: {
         DefaultServer: item('plugin-state.server', 'https://webchem.ncbr.muni.cz/molstar-state'),
@@ -65,6 +68,9 @@ export const PluginConfig = {
         DefaultRepresentationPreset: item<string>('structure.default-representation-preset', 'auto'),
         DefaultRepresentationPresetParams: item<StructureRepresentationPresetProvider.CommonParams>('structure.default-representation-preset-params', { }),
         SaccharideCompIdMapType: item<SaccharideCompIdMapType>('structure.saccharide-comp-id-map-type', 'default'),
+    },
+    Background: {
+        Styles: item<[BackgroundProps, string][]>('background.styles', []),
     }
 };
 
@@ -88,4 +94,4 @@ export class PluginConfigManager {
         if (!initial) return;
         initial.forEach(([k, v]) => this._config.set(k, v));
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-plugin/context.ts b/src/mol-plugin/context.ts
index 29ec1c03d65a88bbff9c669fd86e3eed0a5901b4..fd315a57e306cd784e6d3286fafd8fcf2e26a1a6 100644
--- a/src/mol-plugin/context.ts
+++ b/src/mol-plugin/context.ts
@@ -35,7 +35,7 @@ import { Representation } from '../mol-repr/representation';
 import { StructureRepresentationRegistry } from '../mol-repr/structure/registry';
 import { VolumeRepresentationRegistry } from '../mol-repr/volume/registry';
 import { StateTransform } from '../mol-state';
-import { RuntimeContext, Task } from '../mol-task';
+import { RuntimeContext, Scheduler, Task } from '../mol-task';
 import { ColorTheme } from '../mol-theme/color';
 import { SizeTheme } from '../mol-theme/size';
 import { ThemeRegistryContext } from '../mol-theme/theme';
@@ -71,8 +71,10 @@ export class PluginContext {
     };
 
     protected subs: Subscription[] = [];
+    private initCanvas3dPromiseCallbacks: [res: () => void, rej: (err: any) => void] = [() => {}, () => {}];
 
     private disposed = false;
+    private canvasContainer: HTMLDivElement | undefined = void 0;
     private ev = RxEventHelper.create();
 
     readonly config = new PluginConfigManager(this.spec.config); // needed to init state
@@ -102,10 +104,15 @@ export class PluginContext {
             leftPanelTabName: this.ev.behavior<LeftPanelTabName>('root')
         },
         canvas3d: {
+            // TODO: remove in 4.0?
             initialized: this.canvas3dInit.pipe(filter(v => !!v), take(1))
         }
     } as const;
 
+    readonly canvas3dInitialized = new Promise<void>((res, rej) => {
+        this.initCanvas3dPromiseCallbacks = [res, rej];
+    });
+
     readonly canvas3dContext: Canvas3DContext | undefined;
     readonly canvas3d: Canvas3D | undefined;
     readonly layout = new PluginLayout(this);
@@ -186,6 +193,63 @@ export class PluginContext {
      */
     readonly customState: unknown = Object.create(null);
 
+    initContainer(options?: { canvas3dContext?: Canvas3DContext, checkeredCanvasBackground?: boolean }) {
+        if (this.canvasContainer) return true;
+
+        const container = document.createElement('div');
+        Object.assign(container.style, {
+            position: 'absolute',
+            left: 0,
+            top: 0,
+            right: 0,
+            bottom: 0,
+            '-webkit-user-select': 'none',
+            'user-select': 'none',
+            '-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
+            '-webkit-touch-callout': 'none',
+            'touch-action': 'manipulation',
+        });
+        let canvas = options?.canvas3dContext?.canvas;
+        if (!canvas) {
+            canvas = document.createElement('canvas');
+            if (options?.checkeredCanvasBackground) {
+                Object.assign(canvas.style, {
+                    'background-image': 'linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey), linear-gradient(45deg, lightgrey 25%, transparent 25%, transparent 75%, lightgrey 75%, lightgrey)',
+                    'background-size': '60px 60px',
+                    'background-position': '0 0, 30px 30px'
+                });
+            }
+            container.appendChild(canvas);
+        }
+        if (!this.initViewer(canvas, container, options?.canvas3dContext)) {
+            return false;
+        }
+        this.canvasContainer = container;
+        return true;
+    }
+
+    /**
+     * Mount the plugin into the target element (assumes the target has "relative"-like positioninig).
+     * If initContainer wasn't called separately before, initOptions will be passed to it.
+     */
+    mount(target: HTMLElement, initOptions?: { canvas3dContext?: Canvas3DContext, checkeredCanvasBackground?: boolean }) {
+        if (this.disposed) throw new Error('Cannot mount a disposed context');
+
+        if (!this.initContainer(initOptions)) return false;
+
+        if (this.canvasContainer!.parentElement !== target) {
+            this.canvasContainer!.parentElement?.removeChild(this.canvasContainer!);
+        }
+
+        target.appendChild(this.canvasContainer!);
+        this.handleResize();
+        return true;
+    }
+
+    unmount() {
+        this.canvasContainer?.parentElement?.removeChild(this.canvasContainer);
+    }
+
     initViewer(canvas: HTMLCanvasElement, container: HTMLDivElement, canvas3dContext?: Canvas3DContext) {
         try {
             this.layout.setRoot(container);
@@ -200,8 +264,10 @@ export class PluginContext {
                 const pickScale = this.config.get(PluginConfig.General.PickScale) || 0.25;
                 const pickPadding = this.config.get(PluginConfig.General.PickPadding) ?? 1;
                 const enableWboit = this.config.get(PluginConfig.General.EnableWboit) || false;
+                const enableDpoit = this.config.get(PluginConfig.General.EnableDpoit) || false;
                 const preferWebGl1 = this.config.get(PluginConfig.General.PreferWebGl1) || false;
-                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, preferWebGl1 });
+                const failIfMajorPerformanceCaveat = !(this.config.get(PluginConfig.General.AllowMajorPerformanceCaveat) ?? false);
+                (this.canvas3dContext as Canvas3DContext) = Canvas3DContext.fromCanvas(canvas, this.managers.asset, { antialias, preserveDrawingBuffer, pixelScale, pickScale, pickPadding, enableWboit, enableDpoit, preferWebGl1, failIfMajorPerformanceCaveat });
             }
             (this.canvas3d as Canvas3D) = Canvas3D.create(this.canvas3dContext!);
             this.canvas3dInit.next(true);
@@ -230,10 +296,12 @@ export class PluginContext {
 
             this.handleResize();
 
+            Scheduler.setImmediate(() => this.initCanvas3dPromiseCallbacks[0]());
             return true;
         } catch (e) {
             this.log.error('' + e);
             console.error(e);
+            Scheduler.setImmediate(() => this.initCanvas3dPromiseCallbacks[1](e));
             return false;
         }
     }
@@ -304,6 +372,9 @@ export class PluginContext {
         objectForEach(this.managers, m => (m as any)?.dispose?.());
         objectForEach(this.managers.structure, m => (m as any)?.dispose?.());
 
+        this.unmount();
+        this.canvasContainer = undefined;
+
         this.disposed = true;
     }
 
diff --git a/src/mol-plugin/features.ts b/src/mol-plugin/features.ts
index ad04c71c6287bbae2ca4f8439632503403ea3b71..6a1c06d2206b2329e8f883acb0a8aec9377f08b7 100644
--- a/src/mol-plugin/features.ts
+++ b/src/mol-plugin/features.ts
@@ -1,7 +1,8 @@
 /**
- * Copyright (c) 2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2021-2022 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>
  */
 
 export const PluginFeatureDetection = {
@@ -13,7 +14,7 @@ export const PluginFeatureDetection = {
         const unpportedSafariVersions = [
             'Version/15.1 Safari',
             'Version/15.2 Safari',
-            'Version/15.3 Safari'
+            'Version/15.3 Safari',
         ];
         if (unpportedSafariVersions.some(v => navigator.userAgent.indexOf(v) > 0)) {
             return true;
@@ -28,10 +29,4 @@ export const PluginFeatureDetection = {
         const isTouchScreen = navigator.maxTouchPoints >= 4; // true for iOS 13 (and hopefully beyond)
         return !(window as any).MSStream && (isIOS || (isAppleDevice && isTouchScreen));
     },
-    get wboit() {
-        if (typeof navigator === 'undefined' || typeof window === 'undefined') return true;
-
-        // disable Wboit in Safari 15
-        return !/Version\/15.\d Safari/.test(navigator.userAgent);
-    }
 };
\ No newline at end of file
diff --git a/src/mol-plugin/state.ts b/src/mol-plugin/state.ts
index cf0a05dc34b3f3439e5bde476228cacba2d0a7cd..dedad16d970299b0f5e4f59d538a1938eec4164d 100644
--- a/src/mol-plugin/state.ts
+++ b/src/mol-plugin/state.ts
@@ -124,7 +124,8 @@ class PluginState extends PluginComponent {
     dispose() {
         this.behaviors.cells.forEach(cell => {
             if (PluginBehavior.Behavior.is(cell.obj)) {
-                cell.obj.data.unregister();
+                cell.obj.data.unregister?.();
+                cell.obj.data.dispose?.();
             }
         });
 
diff --git a/src/mol-plugin/util/viewport-screenshot.ts b/src/mol-plugin/util/viewport-screenshot.ts
index 261ae3708d95a5c428b162b35ff69242d306c30c..97368fde642536abe397a736829216e82cbee236 100644
--- a/src/mol-plugin/util/viewport-screenshot.ts
+++ b/src/mol-plugin/util/viewport-screenshot.ts
@@ -309,7 +309,9 @@ class ViewportScreenshotHelper extends PluginComponent {
         if (width <= 0 || height <= 0) return;
 
         await ctx.update('Rendering image...');
-        const imageData = this.imagePass.getImageData(width, height, viewport);
+        const pass = this.imagePass;
+        await pass.updateBackground();
+        const imageData = pass.getImageData(width, height, viewport);
 
         await ctx.update('Encoding image...');
         const canvas = this.canvas;
diff --git a/src/mol-repr/representation.ts b/src/mol-repr/representation.ts
index 246e34eb625d176da85f0221531742c4c93ca499..17da5156448dd9e75ff88ae586a1106c9386825d 100644
--- a/src/mol-repr/representation.ts
+++ b/src/mol-repr/representation.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -154,8 +154,8 @@ interface Representation<D, P extends PD.Params = {}, S extends Representation.S
     createOrUpdate: (props?: Partial<PD.Values<P>>, data?: D) => Task<void>
     setState: (state: Partial<S>) => void
     setTheme: (theme: Theme) => void
-    /** If no pickingId is given, returns a Loci for the whole representation */
-    getLoci: (pickingId?: PickingId) => ModelLoci
+    getLoci: (pickingId: PickingId) => ModelLoci
+    getAllLoci: () => ModelLoci[]
     mark: (loci: ModelLoci, action: MarkerAction) => boolean
     destroy: () => void
 }
@@ -227,6 +227,7 @@ namespace Representation {
         setState: () => {},
         setTheme: () => {},
         getLoci: () => EmptyLoci,
+        getAllLoci: () => [],
         mark: () => false,
         destroy: () => {}
     };
@@ -327,7 +328,7 @@ namespace Representation {
             },
             get state() { return currentState; },
             get theme() { return currentTheme; },
-            getLoci: (pickingId?: PickingId) => {
+            getLoci: (pickingId: PickingId) => {
                 const { visuals } = currentProps;
                 for (let i = 0, il = reprList.length; i < il; ++i) {
                     if (!visuals || visuals.includes(reprMap[i])) {
@@ -337,6 +338,16 @@ namespace Representation {
                 }
                 return EmptyLoci;
             },
+            getAllLoci: () => {
+                const loci: ModelLoci[] = [];
+                const { visuals } = currentProps;
+                for (let i = 0, il = reprList.length; i < il; ++i) {
+                    if (!visuals || visuals.includes(reprMap[i])) {
+                        loci.push(...reprList[i].getAllLoci());
+                    }
+                }
+                return loci;
+            },
             mark: (loci: ModelLoci, action: MarkerAction) => {
                 let marked = false;
                 for (let i = 0, il = reprList.length; i < il; ++i) {
@@ -399,6 +410,10 @@ namespace Representation {
                 // TODO
                 return EmptyLoci;
             },
+            getAllLoci: () => {
+                // TODO
+                return [];
+            },
             mark: (loci: ModelLoci, action: MarkerAction) => {
                 // TODO
                 return false;
diff --git a/src/mol-repr/shape/representation.ts b/src/mol-repr/shape/representation.ts
index 803199b180ff825601d3c1c88b2d5c69eef1f6c7..45c4c68776db007b38a7e0568bd9c0f3c1581095 100644
--- a/src/mol-repr/shape/representation.ts
+++ b/src/mol-repr/shape/representation.ts
@@ -213,14 +213,16 @@ export function ShapeRepresentation<D, G extends Geometry, P extends Geometry.Pa
         get geometryVersion() { return geometryVersion; },
         updated,
         createOrUpdate,
-        getLoci(pickingId?: PickingId) {
-            if (pickingId === undefined) return Shape.Loci(_shape);
+        getLoci(pickingId: PickingId) {
             const { objectId, groupId, instanceId } = pickingId;
             if (_renderObject && _renderObject.id === objectId) {
                 return ShapeGroup.Loci(_shape, [{ ids: OrderedSet.ofSingleton(groupId), instance: instanceId }]);
             }
             return EmptyLoci;
         },
+        getAllLoci() {
+            return [Shape.Loci(_shape)];
+        },
         mark(loci: Loci, action: MarkerAction) {
             if (!MarkerActions.is(_state.markerActions, action)) return false;
             if (ShapeGroup.isLoci(loci) || Shape.isLoci(loci)) {
diff --git a/src/mol-repr/structure/complex-representation.ts b/src/mol-repr/structure/complex-representation.ts
index f1edcbae4f58c5fe4938f2c4e2f2923ecf27b7a3..a50d04ae0c47bda3e592bfd93e2aa2b25dcc4de3 100644
--- a/src/mol-repr/structure/complex-representation.ts
+++ b/src/mol-repr/structure/complex-representation.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -72,11 +72,14 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         });
     }
 
-    function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure.target);
+    function getLoci(pickingId: PickingId) {
         return visual ? visual.getLoci(pickingId) : EmptyLoci;
     }
 
+    function getAllLoci() {
+        return [Structure.Loci(_structure.target)];
+    }
+
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
@@ -157,6 +160,7 @@ export function ComplexRepresentation<P extends StructureParams>(label: string,
         setState,
         setTheme,
         getLoci,
+        getAllLoci,
         mark,
         destroy
     };
diff --git a/src/mol-repr/structure/units-representation.ts b/src/mol-repr/structure/units-representation.ts
index bc3130700ca2fe75673b471f9b4a3aa4ff634369..d7a2b6e0634e71156342aff34217ec67b23ac55b 100644
--- a/src/mol-repr/structure/units-representation.ts
+++ b/src/mol-repr/structure/units-representation.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  * @author David Sehnal <david.sehnal@gmail.com>
@@ -185,8 +185,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         });
     }
 
-    function getLoci(pickingId?: PickingId) {
-        if (pickingId === undefined) return Structure.Loci(_structure.target);
+    function getLoci(pickingId: PickingId) {
         let loci: Loci = EmptyLoci;
         visuals.forEach(({ visual }) => {
             const _loci = visual.getLoci(pickingId);
@@ -195,6 +194,10 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         return loci;
     }
 
+    function getAllLoci() {
+        return [Structure.Loci(_structure.target)];
+    }
+
     function mark(loci: Loci, action: MarkerAction) {
         if (!_structure) return false;
         if (!MarkerActions.is(_state.markerActions, action)) return false;
@@ -302,6 +305,7 @@ export function UnitsRepresentation<P extends StructureParams>(label: string, ct
         setState,
         setTheme,
         getLoci,
+        getAllLoci,
         mark,
         destroy
     };
diff --git a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
index 884f7045262d152833701362bb9ae9b3d9632e50..85da9b6064cf3e6d59dd19a88ccc118a92fbb152 100644
--- a/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
+++ b/src/mol-repr/structure/visual/gaussian-surface-mesh.ts
@@ -28,6 +28,7 @@ import { applyTextureMeshColorSmoothing } from '../../../mol-geo/geometry/textur
 import { ColorSmoothingParams, getColorSmoothingProps } from '../../../mol-geo/geometry/base';
 import { Vec3 } from '../../../mol-math/linear-algebra';
 import { isTimingMode } from '../../../mol-util/debug';
+import { ValueCell } from '../../../mol-util/value-cell';
 
 const SharedParams = {
     ...GaussianDensityParams,
@@ -101,7 +102,12 @@ async function createGaussianSurfaceMesh(ctx: VisualContext, unit: Unit, structu
     (surface.meta.resolution as GaussianSurfaceMeta['resolution']) = resolution;
 
     Mesh.transform(surface, transform);
-    if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
+    if (ctx.webgl && !ctx.webgl.isWebGL2) {
+        Mesh.uniformTriangleGroup(surface);
+        ValueCell.updateIfChanged(surface.varyingGroup, false);
+    } else {
+        ValueCell.updateIfChanged(surface.varyingGroup, true);
+    }
 
     const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxRadius);
     surface.setBoundingSphere(sphere);
@@ -162,7 +168,12 @@ async function createStructureGaussianSurfaceMesh(ctx: VisualContext, structure:
     (surface.meta.resolution as GaussianSurfaceMeta['resolution']) = resolution;
 
     Mesh.transform(surface, transform);
-    if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
+    if (ctx.webgl && !ctx.webgl.isWebGL2) {
+        Mesh.uniformTriangleGroup(surface);
+        ValueCell.updateIfChanged(surface.varyingGroup, false);
+    } else {
+        ValueCell.updateIfChanged(surface.varyingGroup, true);
+    }
 
     const sphere = Sphere3D.expand(Sphere3D(), structure.boundary.sphere, maxRadius);
     surface.setBoundingSphere(sphere);
@@ -229,7 +240,7 @@ async function createGaussianSurfaceTextureMesh(ctx: VisualContext, unit: Unit,
 
     const axisOrder = Vec3.create(0, 1, 2);
     const buffer = textureMesh?.doubleBuffer.get();
-    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, buffer?.vertex, buffer?.group, buffer?.normal);
+    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
     if (isTimingMode) ctx.webgl.timer.markEnd('createGaussianSurfaceTextureMesh');
 
     const groupCount = unit.elements.length;
@@ -303,7 +314,7 @@ async function createStructureGaussianSurfaceTextureMesh(ctx: VisualContext, str
 
     const axisOrder = Vec3.create(0, 1, 2);
     const buffer = textureMesh?.doubleBuffer.get();
-    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, buffer?.vertex, buffer?.group, buffer?.normal);
+    const gv = extractIsosurface(ctx.webgl, densityTextureData.texture, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.gridTexScale, densityTextureData.transform, isoLevel, false, true, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
     if (isTimingMode) ctx.webgl.timer.markEnd('createStructureGaussianSurfaceTextureMesh');
 
     const groupCount = structure.elementCount;
diff --git a/src/mol-repr/structure/visual/molecular-surface-mesh.ts b/src/mol-repr/structure/visual/molecular-surface-mesh.ts
index 173a5181c1111e935b0ac9ab3ba3bcefccc3fb7b..72e1a972c6f5c04d0eac95cc39527cab2406c5a5 100644
--- a/src/mol-repr/structure/visual/molecular-surface-mesh.ts
+++ b/src/mol-repr/structure/visual/molecular-surface-mesh.ts
@@ -22,6 +22,7 @@ import { Texture } from '../../../mol-gl/webgl/texture';
 import { WebGLContext } from '../../../mol-gl/webgl/context';
 import { applyMeshColorSmoothing } from '../../../mol-geo/geometry/mesh/color-smoothing';
 import { ColorSmoothingParams, getColorSmoothingProps } from '../../../mol-geo/geometry/base';
+import { ValueCell } from '../../../mol-util';
 
 export const MolecularSurfaceMeshParams = {
     ...UnitsMeshParams,
@@ -55,7 +56,12 @@ async function createMolecularSurfaceMesh(ctx: VisualContext, unit: Unit, struct
     }
 
     Mesh.transform(surface, transform);
-    if (ctx.webgl && !ctx.webgl.isWebGL2) Mesh.uniformTriangleGroup(surface);
+    if (ctx.webgl && !ctx.webgl.isWebGL2) {
+        Mesh.uniformTriangleGroup(surface);
+        ValueCell.updateIfChanged(surface.varyingGroup, false);
+    } else {
+        ValueCell.updateIfChanged(surface.varyingGroup, true);
+    }
 
     const sphere = Sphere3D.expand(Sphere3D(), unit.boundary.sphere, maxRadius);
     surface.setBoundingSphere(sphere);
diff --git a/src/mol-repr/volume/isosurface.ts b/src/mol-repr/volume/isosurface.ts
index 7f413cc883337afa8c69d84759dd710d2a853abd..e99c46d855e6991d2c794735e01916f9fa4192b9 100644
--- a/src/mol-repr/volume/isosurface.ts
+++ b/src/mol-repr/volume/isosurface.ts
@@ -29,6 +29,7 @@ import { WebGLContext } from '../../mol-gl/webgl/context';
 import { CustomPropertyDescriptor } from '../../mol-model/custom-property';
 import { Texture } from '../../mol-gl/webgl/texture';
 import { BaseGeometry } from '../../mol-geo/geometry/base';
+import { ValueCell } from '../../mol-util/value-cell';
 
 export const VolumeIsosurfaceParams = {
     isoValue: Volume.IsoValueParam
@@ -65,8 +66,16 @@ function getLoci(volume: Volume, props: VolumeIsosurfaceProps) {
 
 function getIsosurfaceLoci(pickingId: PickingId, volume: Volume, props: VolumeIsosurfaceProps, id: number) {
     const { objectId, groupId } = pickingId;
+
     if (id === objectId) {
-        return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+        const granularity = Volume.PickingGranularity.get(volume);
+        if (granularity === 'volume') {
+            return Volume.Loci(volume);
+        } else if (granularity === 'object') {
+            return Volume.Isosurface.Loci(volume, props.isoValue);
+        } else {
+            return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+        }
     }
     return EmptyLoci;
 }
@@ -94,6 +103,9 @@ export async function createVolumeIsosurfaceMesh(ctx: VisualContext, volume: Vol
         // 2nd arg means not to split triangles based on group id. Splitting triangles
         // is too expensive if each cell has its own group id as is the case here.
         Mesh.uniformTriangleGroup(surface, false);
+        ValueCell.updateIfChanged(surface.varyingGroup, false);
+    } else {
+        ValueCell.updateIfChanged(surface.varyingGroup, true);
     }
 
     surface.setBoundingSphere(Volume.getBoundingSphere(volume));
@@ -185,7 +197,7 @@ async function createVolumeIsosurfaceTextureMesh(ctx: VisualContext, volume: Vol
 
     const axisOrder = volume.grid.cells.space.axisOrderSlowToFast as Vec3;
     const buffer = textureMesh?.doubleBuffer.get();
-    const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, buffer?.vertex, buffer?.group, buffer?.normal);
+    const gv = extractIsosurface(ctx.webgl, texture, gridDimension, gridTexDim, gridTexScale, transform, isoLevel, value < 0, false, axisOrder, true, buffer?.vertex, buffer?.group, buffer?.normal);
 
     const groupCount = volume.grid.cells.data.length;
     const surface = TextureMesh.create(gv.vertexCount, groupCount, gv.vertexTexture, gv.groupTexture, gv.normalTexture, Volume.getBoundingSphere(volume), textureMesh);
diff --git a/src/mol-repr/volume/representation.ts b/src/mol-repr/volume/representation.ts
index fae57da2554529ff0eee4d13a229d8cc6c956271..c6de383731b5f36a097cd321d19660c7e4502651 100644
--- a/src/mol-repr/volume/representation.ts
+++ b/src/mol-repr/volume/representation.ts
@@ -358,10 +358,12 @@ export function VolumeRepresentation<P extends VolumeParams>(label: string, ctx:
         createOrUpdate,
         setState,
         setTheme,
-        getLoci: (pickingId?: PickingId): Loci => {
-            if (pickingId === undefined) return getLoci(_volume, _props);
+        getLoci: (pickingId: PickingId): Loci => {
             return visual ? visual.getLoci(pickingId) : EmptyLoci;
         },
+        getAllLoci: (): Loci[] => {
+            return [getLoci(_volume, _props)];
+        },
         mark,
         destroy
     };
diff --git a/src/mol-repr/volume/slice.ts b/src/mol-repr/volume/slice.ts
index 460f7881736f49bc119de7d554734cc96f2bd11e..d81bb7c513893aad48a17d3b94cf6f69e6cbb911 100644
--- a/src/mol-repr/volume/slice.ts
+++ b/src/mol-repr/volume/slice.ts
@@ -150,7 +150,14 @@ function getLoci(volume: Volume, props: PD.Values<SliceParams>) {
 function getSliceLoci(pickingId: PickingId, volume: Volume, props: PD.Values<SliceParams>, id: number) {
     const { objectId, groupId } = pickingId;
     if (id === objectId) {
-        return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+        const granularity = Volume.PickingGranularity.get(volume);
+        if (granularity === 'volume') {
+            return Volume.Loci(volume);
+        } if (granularity === 'object') {
+            return getLoci(volume, props);
+        } else {
+            return Volume.Cell.Loci(volume, Interval.ofSingleton(groupId as Volume.CellIndex));
+        }
     }
     return EmptyLoci;
 }
diff --git a/src/mol-script/language/symbol-table/structure-query.ts b/src/mol-script/language/symbol-table/structure-query.ts
index 9e765f41649288a054b01a69b4a6a7d06b1c2bf6..209da4376e81c413caada96b2a2b544146e3de48 100644
--- a/src/mol-script/language/symbol-table/structure-query.ts
+++ b/src/mol-script/language/symbol-table/structure-query.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -333,6 +333,7 @@ const bondProperty = {
 
     flags: bondProp(Types.BondFlags),
     order: bondProp(Type.Num),
+    key: bondProp(Type.Num),
     length: bondProp(Type.Num),
     atomA: bondProp(Types.ElementReference),
     atomB: bondProp(Types.ElementReference)
@@ -356,5 +357,5 @@ export const structureQuery = {
     combinator,
     atomSet,
     atomProperty,
-    bondProperty: bondProperty
+    bondProperty
 };
\ No newline at end of file
diff --git a/src/mol-script/runtime/query/table.ts b/src/mol-script/runtime/query/table.ts
index dc32f889628e0958a518ed074a31cbb8390d4780..deab2d4baa0c10ab7fc3641b258ef8a1c7a0cce2 100644
--- a/src/mol-script/runtime/query/table.ts
+++ b/src/mol-script/runtime/query/table.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018-2019 Mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2022 Mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
@@ -211,21 +211,21 @@ const symbols = [
     // ============= FILTERS ================
     D(MolScript.structureQuery.filter.pick, (ctx, xs) => Queries.filters.pick(xs[0] as any, xs['test'])(ctx)),
     D(MolScript.structureQuery.filter.first, (ctx, xs) => Queries.filters.first(xs[0] as any)(ctx)),
-    D(MolScript.structureQuery.filter.withSameAtomProperties, (ctx, xs) => Queries.filters.withSameAtomProperties(xs[0] as any, xs['source'] as any, xs['property'] as any)(ctx)),
+    D(MolScript.structureQuery.filter.withSameAtomProperties, (ctx, xs) => Queries.filters.withSameAtomProperties(xs[0] as any, xs['source'] as any, xs['property'])(ctx)),
     D(MolScript.structureQuery.filter.intersectedBy, (ctx, xs) => Queries.filters.areIntersectedBy(xs[0] as any, xs['by'] as any)(ctx)),
     D(MolScript.structureQuery.filter.within, (ctx, xs) => Queries.filters.within({
         query: xs[0] as any,
         target: xs['target'] as any,
-        minRadius: xs['min-radius'] as any,
-        maxRadius: xs['max-radius'] as any,
+        minRadius: xs['min-radius']?.(ctx) as any,
+        maxRadius: xs['max-radius']?.(ctx) as any,
         elementRadius: xs['atom-radius'] as any,
-        invert: xs['invert'] as any
+        invert: xs['invert']?.(ctx) as any
     })(ctx)),
     D(MolScript.structureQuery.filter.isConnectedTo, (ctx, xs) => Queries.filters.isConnectedTo({
         query: xs[0] as any,
         target: xs['target'] as any,
-        disjunct: xs['disjunct'] as any,
-        invert: xs['invert'] as any,
+        disjunct: xs['disjunct']?.(ctx) as any,
+        invert: xs['invert']?.(ctx) as any,
         bondTest: xs['bond-test']
     })(ctx)),
 
@@ -248,6 +248,9 @@ const symbols = [
     D(MolScript.structureQuery.generator.rings, function structureQuery_generator_rings(ctx, xs) {
         return Queries.generators.rings(xs?.['fingerprint']?.(ctx) as any, xs?.['only-aromatic']?.(ctx))(ctx);
     }),
+    D(MolScript.structureQuery.generator.queryInSelection, function structureQuery_generator_queryInSelection(ctx, xs) {
+        return Queries.generators.querySelection(xs[0] as any, xs['query'] as any, xs['in-complement']?.(ctx) as any)(ctx);
+    }),
 
     // ============= MODIFIERS ================
 
@@ -278,6 +281,7 @@ const symbols = [
             fixedPoint: xs['fixed-point']?.(ctx) ?? false
         })(ctx);
     }),
+    D(MolScript.structureQuery.modifier.intersectBy, function structureQuery_modifier_intersectBy(ctx, xs) { return Queries.modifiers.intersectBy(xs[0] as any, xs['by'] as any)(ctx); }),
 
     // ============= COMBINATORS ================
 
@@ -353,9 +357,27 @@ const symbols = [
     D(MolScript.structureQuery.atomProperty.macromolecular.secondaryStructureFlags, atomProp(StructureProperties.residue.secondary_structure_type)),
     D(MolScript.structureQuery.atomProperty.macromolecular.chemCompType, atomProp(StructureProperties.residue.chem_comp_type)),
 
+    // ============= ATOM SET ================
+
+    D(MolScript.structureQuery.atomSet.atomCount,
+        function structureQuery_atomset_atomCount(ctx, xs) {
+	    return Queries.atomset.atomCount(ctx);
+        }),
+
+    D(MolScript.structureQuery.atomSet.countQuery,
+        function structureQuery_atomset_countQuery(ctx, xs) {
+	    return Queries.atomset.countQuery(xs[0] as any)(ctx);
+        }),
+
+    D(MolScript.structureQuery.atomSet.propertySet,
+        function structureQuery_atomset_propertySet(ctx, xs) {
+	  return Queries.atomset.propertySet(xs[0] as any)(ctx);
+        }),
+
     // ============= BOND PROPERTIES ================
     D(MolScript.structureQuery.bondProperty.order, (ctx, xs) => ctx.atomicBond.order),
     D(MolScript.structureQuery.bondProperty.flags, (ctx, xs) => ctx.atomicBond.type),
+    D(MolScript.structureQuery.bondProperty.key, (ctx, xs) => ctx.atomicBond.key),
     D(MolScript.structureQuery.bondProperty.atomA, (ctx, xs) => ctx.atomicBond.a),
     D(MolScript.structureQuery.bondProperty.atomB, (ctx, xs) => ctx.atomicBond.b),
     D(MolScript.structureQuery.bondProperty.length, (ctx, xs) => ctx.atomicBond.length),
@@ -406,4 +428,4 @@ function getArray<T = any>(ctx: QueryContext, xs: any): T[] {
     for (const s of symbols) {
         DefaultQueryRuntimeTable.addSymbol(s);
     }
-})();
\ No newline at end of file
+})();
diff --git a/src/mol-script/script.ts b/src/mol-script/script.ts
index 756861c6a62a118da82c2347023f3854f8480e8c..7497f49b82e83461416930d50d330aaf584eb911 100644
--- a/src/mol-script/script.ts
+++ b/src/mol-script/script.ts
@@ -6,10 +6,12 @@
 
 import { transpileMolScript } from './script/mol-script/symbols';
 import { parseMolScript } from './language/parser';
+import { parse } from './transpile';
 import { Expression } from './language/expression';
 import { StructureElement, QueryContext, StructureSelection, Structure, QueryFn, QueryContextOptions } from '../mol-model/structure';
 import { compile } from './runtime/query/compiler';
 import { MolScriptBuilder } from './language/builder';
+import { assertUnreachable } from '../mol-util/type-helpers';
 
 export { Script };
 
@@ -20,7 +22,13 @@ function Script(expression: string, language: Script.Language): Script {
 }
 
 namespace Script {
-    export type Language = 'mol-script'
+    export const Info = {
+        'mol-script': 'Mol-Script',
+        'pymol': 'PyMOL',
+        'vmd': 'VMD',
+        'jmol': 'Jmol',
+    };
+    export type Language = keyof typeof Info;
 
     export function is(x: any): x is Script {
         return !!x && typeof (x as Script).expression === 'string' && !!(x as Script).language;
@@ -36,8 +44,13 @@ namespace Script {
                 const parsed = parseMolScript(script.expression);
                 if (parsed.length === 0) throw new Error('No query');
                 return transpileMolScript(parsed[0]);
+            case 'pymol':
+            case 'jmol':
+            case 'vmd':
+                return parse(script.language, script.expression);
+            default:
+                assertUnreachable(script.language);
         }
-        throw new Error('unsupported script language');
     }
 
     export function toQuery(script: Script): QueryFn<StructureSelection> {
@@ -56,4 +69,4 @@ namespace Script {
         const query = compile<StructureSelection>(e);
         return query(new QueryContext(structure, options));
     }
-}
\ No newline at end of file
+}
diff --git a/src/mol-script/transpile.ts b/src/mol-script/transpile.ts
new file mode 100644
index 0000000000000000000000000000000000000000..31395308bfe9e56fffbccf15b656403e47002fb5
--- /dev/null
+++ b/src/mol-script/transpile.ts
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL src/transpile.ts
+ */
+
+import { Transpiler } from './transpilers/transpiler';
+import { _transpiler } from './transpilers/all';
+import { Expression } from './language/expression';
+import { Script } from './script';
+const transpiler: {[index: string]: Transpiler} = _transpiler;
+
+export function parse(lang: Script.Language, str: string): Expression {
+    try {
+
+        const query = transpiler[lang](str);
+        return query;
+
+    } catch (e) {
+
+        console.error(e.message);
+        throw e;
+
+    }
+}
diff --git a/src/mol-script/transpilers/_spec/examples.spec.ts b/src/mol-script/transpilers/_spec/examples.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ca45ce7e2a519950e1a792da8aa81f646b21c87d
--- /dev/null
+++ b/src/mol-script/transpilers/_spec/examples.spec.ts
@@ -0,0 +1,26 @@
+/**
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ * Adapted from MolQL project
+**/
+
+import { Transpiler } from '../transpiler';
+import { _transpiler as transpilers } from '../all';
+
+function testTranspilerExamples(name: string, transpiler: Transpiler) {
+    describe(`${name} examples`, () => {
+        const examples = require(`../${name}/examples`).examples;
+        //        console.log(examples);
+        for (const e of examples) {
+
+            it(e.name, () => {
+                // check if it transpiles and compiles/typechecks.
+                transpiler(e.value);
+            });
+        }
+    });
+}
+
+testTranspilerExamples('pymol', transpilers.pymol);
+testTranspilerExamples('vmd', transpilers.vmd);
+testTranspilerExamples('jmol', transpilers.jmol);
diff --git a/src/mol-script/transpilers/_spec/jmol.spec.ts b/src/mol-script/transpilers/_spec/jmol.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4dabd8383b60bde3259d074116043e9e0d2f8795
--- /dev/null
+++ b/src/mol-script/transpilers/_spec/jmol.spec.ts
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2020-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as u from './utils';
+import { transpiler } from '../jmol/parser';
+import { keywords } from '../jmol/keywords';
+import { properties } from '../jmol/properties';
+import { operators } from '../jmol/operators';
+
+const general = {
+    supported: [
+        // atom expressions
+        '123',
+        '-42',
+        '_C',
+        '.CA',
+        'ALA',
+        '%A',
+        '^B',
+        ':C',
+        '/2',
+        '10^A:F.CA%C/0',
+        '10^A:F.CA%C',
+        '10^A:F.CA',
+        '10^A:F',
+        '10^A',
+        '10:F.CA',
+        '10/0',
+        '32 or 42',
+        '.CA/0 OR 42:A',
+        '!23',
+        'not ASP',
+        '(ASP or .CA)',
+        'ASP and .CA',
+        '123.CA',
+        '(1 or 2) and .CA',
+        '(1 or 2) and (.CA or .N)',
+        '.CA and (2 or 3)',
+        '.CA and (2 or 3) and ^A',
+        '!32 or :A and .CA',
+
+        // trimming
+        '    atomName = CA   ',
+        'atomName = CA   ',
+        '    atomName = CA',
+
+        // value comparison
+        'resno > 10',
+        // atom expression
+        '[LEU]100:A.CA',
+        '[LEU]100:A',
+        '[LEU]100.CA',
+        '[LEU]:A.CA',
+        '[LEU].CA',
+        // comma as OR
+        '100, 42, ALA',
+        // residue numbering
+        '(1-10,15,21-30)',
+        // within
+        'within(5,[HEM])',
+        // within with parentheses
+        '(within(5,[HEM])) and backbone',
+        '( within(5,[HEM]) ) and backbone',
+        // trimming
+        '[ALA] and [VAL]  ',
+        ' [ALA] and [VAL] ',
+        '  [ALA] and [VAL]',
+        // within with whitespaces
+        'within (   5 ,  [HEM] ) ',
+        // un-braketed residue name
+        'LEU and ILE',
+        // un-parenthesized residue index range
+        '100-120,220',
+        // un-parenthesized residue index
+        '20',
+        // within in the head or the middle of sentence
+        'within (   5 ,  [HEM] ) and backbone',
+
+        // atom expressions with ranges
+        '19-32:A',
+        '-2-32:B',
+        '-10--2:C',
+        '[1FO]19-32:A',
+    ],
+    unsupported: [
+        // values outside of comparisons
+        'foobar',
+        'protein or foobar',
+    ]
+};
+
+describe('jmol general', () => {
+    general.supported.forEach(str => {
+        it(str, () => {
+            transpiler(str);
+        });
+    });
+    general.unsupported.forEach(str => {
+        it(str, () => {
+            const transpileStr = () => transpiler(str);
+            expect(transpileStr).toThrow();
+            expect(transpileStr).not.toThrowError(RangeError);
+        });
+    });
+});
+
+describe('jmol keywords', () => u.testKeywords(keywords, transpiler));
+describe('jmol properties', () => u.testProperties(properties, transpiler));
+describe('jmol operators', () => u.testOperators(operators, transpiler));
diff --git a/src/mol-script/transpilers/_spec/pymol.spec.ts b/src/mol-script/transpilers/_spec/pymol.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..3a463f2122ed7f21b9c63a6e7465cc34c3112db0
--- /dev/null
+++ b/src/mol-script/transpilers/_spec/pymol.spec.ts
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ */
+
+import * as u from './utils';
+import { transpiler } from '../pymol/parser';
+import { keywords } from '../pymol/keywords';
+import { properties } from '../pymol/properties';
+import { operators } from '../pymol/operators';
+
+const general = {
+    supported: [
+        // macros
+        '10/cb',
+        'a/10-12/ca',
+        'lig/b/6+8/c+o',
+
+        // trimming
+        '    name CA   ',
+        'name CA   ',
+        '    name CA',
+    ],
+    unsupported: [
+        // macros
+        'pept/enz/c/3/n',
+        'pept/enz///n',
+
+        '/pept/lig/',
+        '/pept/lig/a',
+        '/pept/lig/a/10',
+        '/pept/lig/a/10/ca',
+        '/pept//a/10',
+
+        // object
+        'foobar',
+        'protein and bazbar',
+    ]
+};
+
+describe('pymol general', () => {
+    general.supported.forEach(str => {
+        it(str, () => {
+            transpiler(str);
+            //          compile(expr);
+        });
+    });
+    general.unsupported.forEach(str => {
+        it(str, () => {
+            const transpileStr = () => transpiler(str);
+            expect(transpileStr).toThrow();
+            expect(transpileStr).not.toThrowError(RangeError);
+        });
+    });
+});
+
+// check against builder output
+// 'not (resi 42 or chain A)'
+// '!resi 42 or chain A'
+// 'b >= 0.3',
+// 'b != 0.3',
+// 'b>0.3',
+// 'b <0.3',
+// 'b <= 0.3',
+// 'b = 1',
+// 'fc.=.1',
+
+describe('pymol keywords', () => u.testKeywords(keywords, transpiler));
+describe('pymol operators', () => u.testOperators(operators, transpiler));
+describe('pymol properties', () => u.testProperties(properties, transpiler));
diff --git a/src/mol-script/transpilers/_spec/utils.ts b/src/mol-script/transpilers/_spec/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..9e0d6b59d269ccf1e3a28ddf64fa56db1c2d6a6f
--- /dev/null
+++ b/src/mol-script/transpilers/_spec/utils.ts
@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panangiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ */
+
+import { Transpiler } from '../transpiler';
+import { KeywordDict, PropertyDict, OperatorList } from '../types';
+
+export function testKeywords(keywords: KeywordDict, transpiler: Transpiler) {
+    for (const name in keywords) {
+        it(name, () => {
+            const k = keywords[name];
+            if (k.map) {
+                const expr = transpiler(name);
+                expect(expr).toEqual(k.map());
+            } else {
+                const transpile = () => transpiler(name);
+                expect(transpile).toThrow();
+                expect(transpile).not.toThrowError(RangeError);
+            }
+        });
+    }
+}
+
+export function testProperties(properties: PropertyDict, transpiler: Transpiler) {
+    for (const name in properties) {
+        const p = properties[name];
+        p['@examples'].forEach(example => {
+            it(name, () => {
+                if (!p.isUnsupported) {
+                    transpiler(example);
+                } else {
+                    const transpile = () => transpiler(example);
+                    expect(transpile).toThrow();
+                    expect(transpile).not.toThrowError(RangeError);
+                }
+            });
+        });
+        it(name, () => {
+            if (!p['@examples'].length) {
+                throw Error(`'${name}' property has no example(s)`);
+            }
+        });
+    }
+}
+
+export function testOperators(operators: OperatorList, transpiler: Transpiler) {
+    operators.forEach(o => {
+        o['@examples'].forEach(example => {
+            it(o.name, () => {
+                if (!o.isUnsupported) {
+                    transpiler(example);
+                } else {
+                    const transpile = () => transpiler(example);
+                    expect(transpile).toThrow();
+                    expect(transpile).not.toThrowError(RangeError);
+                }
+            });
+        });
+        it(o.name, () => {
+            if (!o['@examples'].length) {
+                throw Error(`'${o.name}' operator has no example(s)`);
+            }
+        });
+    });
+}
diff --git a/src/mol-script/transpilers/_spec/vmd.spec.ts b/src/mol-script/transpilers/_spec/vmd.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1da4b9f58a742cec0fb9eed84f89cfad17f36a83
--- /dev/null
+++ b/src/mol-script/transpilers/_spec/vmd.spec.ts
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2020-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ */
+
+import * as u from './utils';
+import { transpiler } from '../vmd/parser';
+import { keywords } from '../vmd/keywords';
+import { properties } from '../vmd/properties';
+import { operators } from '../vmd/operators';
+
+const general = {
+    supported: [
+        // trimming
+        '    name CA   ',
+        'name CA   ',
+        '    name CA',
+    ],
+    unsupported: [
+        // variables
+        'name $atomname',
+        'protein and @myselection',
+
+        // values outside of comparisons
+        'foobar',
+        '34',
+        'name',
+        'abs(-42)',
+        'abs(21+21)',
+        'sqr(3)',
+        'sqr(x)',
+        'sqr(x+33)',
+        'protein or foobar',
+        '34 and protein',
+        'name or protein',
+    ]
+};
+
+describe('vmd general', () => {
+    general.supported.forEach(str => {
+        it(str, () => {
+            transpiler(str);
+            // compile(expr);
+        });
+    });
+    general.unsupported.forEach(str => {
+        it(str, () => {
+            const transpileStr = () => transpiler(str);
+            expect(transpileStr).toThrow();
+            expect(transpileStr).not.toThrowError(RangeError);
+        });
+    });
+});
+
+describe('vmd keywords', () => u.testKeywords(keywords, transpiler));
+describe('vmd operators', () => u.testOperators(operators, transpiler));
+describe('vmd properties', () => u.testProperties(properties, transpiler));
diff --git a/src/mol-script/transpilers/all.ts b/src/mol-script/transpilers/all.ts
new file mode 100644
index 0000000000000000000000000000000000000000..01f98d6643f9a8ae9d39460cb13d7092301121d9
--- /dev/null
+++ b/src/mol-script/transpilers/all.ts
@@ -0,0 +1,17 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { transpiler as jmol } from './jmol/parser';
+import { transpiler as pymol } from './pymol/parser';
+import { transpiler as vmd } from './vmd/parser';
+
+export const _transpiler = {
+    pymol,
+    vmd,
+    jmol,
+};
diff --git a/src/mol-script/transpilers/helper.ts b/src/mol-script/transpilers/helper.ts
new file mode 100644
index 0000000000000000000000000000000000000000..03b3d583aedf184e59595d37f5361b9b72c4167b
--- /dev/null
+++ b/src/mol-script/transpilers/helper.ts
@@ -0,0 +1,385 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../mol-util/monadic-parser';
+import { MolScriptBuilder } from '../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { Expression } from '../language/expression';
+import { KeywordDict, PropertyDict, FunctionDict, OperatorList } from './types';
+
+export function escapeRegExp(s: String) {
+    return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
+}
+
+// Takes a parser for the prefix operator, and a parser for the base thing being
+// parsed, and parses as many occurrences as possible of the prefix operator.
+// Note that the parser is created using `P.lazy` because it's recursive. It's
+// valid for there to be zero occurrences of the prefix operator.
+export function prefix(opParser: P.MonadicParser<any>, nextParser: P.MonadicParser<any>, mapFn: any) {
+    const parser: P.MonadicParser<any> = P.MonadicParser.lazy(() => {
+        return P.MonadicParser.seq(opParser, parser)
+            .map(x => mapFn(...x))
+            .or(nextParser);
+    });
+    return parser;
+}
+
+// Ideally this function would be just like `PREFIX` but reordered like
+// `P.seq(parser, opParser).or(nextParser)`, but that doesn't work. The
+// reason for that is that Parsimmon will get stuck in infinite recursion, since
+// the very first rule. Inside `parser` is to match parser again. Alternatively,
+// you might think to try `nextParser.or(P.seq(parser, opParser))`, but
+// that won't work either because in a call to `.or` (aka `P.alt`), Parsimmon
+// takes the first possible match, even if subsequent matches are longer, so the
+// parser will never actually look far enough ahead to see the postfix
+// operators.
+export function postfix(opParser: P.MonadicParser<any>, nextParser: P.MonadicParser<any>, mapFn: any) {
+    // Because we can't use recursion like stated above, we just match a flat list
+    // of as many occurrences of the postfix operator as possible, then use
+    // `.reduce` to manually nest the list.
+    //
+    // Example:
+    //
+    // INPUT  :: "4!!!"
+    // PARSE  :: [4, "factorial", "factorial", "factorial"]
+    // REDUCE :: ["factorial", ["factorial", ["factorial", 4]]]
+    return P.MonadicParser.seqMap(
+        nextParser,
+        opParser.many(),
+        (x: any, suffixes: any) =>
+            suffixes.reduce((acc: any, x: any) => {
+                return mapFn(x, acc);
+            }, x)
+    );
+}
+
+// Takes a parser for all the operators at this precedence level, and a parser
+// that parsers everything at the next precedence level, and returns a parser
+// that parses as many binary operations as possible, associating them to the
+// right. (e.g. 1^2^3 is 1^(2^3) not (1^2)^3)
+export function binaryRight(opParser: P.MonadicParser<any>, nextParser: P.MonadicParser<any>, mapFn: any) {
+    const parser: P.MonadicParser<any> = P.MonadicParser.lazy(() =>
+        nextParser.chain(next =>
+            P.MonadicParser.seq(
+                opParser,
+                P.MonadicParser.of(next),
+                parser
+            ).map((x) => {
+                return x;
+            }).or(P.MonadicParser.of(next))
+        )
+    );
+    return parser;
+}
+
+// Takes a parser for all the operators at this precedence level, and a parser
+// that parsers everything at the next precedence level, and returns a parser
+// that parses as many binary operations as possible, associating them to the
+// left. (e.g. 1-2-3 is (1-2)-3 not 1-(2-3))
+export function binaryLeft(opParser: P.MonadicParser<any>, nextParser: P.MonadicParser<any>, mapFn: any) {
+    // We run into a similar problem as with the `POSTFIX` parser above where we
+    // can't recurse in the direction we want, so we have to resort to parsing an
+    // entire list of operator chunks and then using `.reduce` to manually nest
+    // them again.
+    //
+    // Example:
+    //
+    // INPUT  :: "1+2+3"
+    // PARSE  :: [1, ["+", 2], ["+", 3]]
+    // REDUCE :: ["+", ["+", 1, 2], 3]
+    return P.MonadicParser.seqMap(
+        nextParser,
+        P.MonadicParser.seq(opParser, nextParser).many(),
+        (first: any, rest: any) => {
+            return rest.reduce((acc: any, ch: any) => {
+                const [op, another] = ch;
+                return mapFn(op, acc, another);
+            }, first);
+        }
+    );
+}
+
+/**
+ * combine operators of decreasing binding strength
+ */
+export function combineOperators(opList: any[], rule: P.MonadicParser<any>) {
+    const x = opList.reduce(
+        (acc, level) => {
+            const map = level.isUnsupported ? makeError(`operator '${level.name}' not supported`) : level.map;
+            return level.type(level.rule, acc, map);
+        },
+        rule
+    );
+    return x;
+}
+
+export function infixOp(re: RegExp, group: number = 0) {
+    return P.MonadicParser.optWhitespace.then(P.MonadicParser.regexp(re, group).skip(P.MonadicParser.optWhitespace));
+}
+
+export function prefixOp(re: RegExp, group: number = 0) {
+    return P.MonadicParser.regexp(re, group).skip(P.MonadicParser.optWhitespace);
+}
+
+export function postfixOp(re: RegExp, group: number = 0) {
+    return P.MonadicParser.optWhitespace.then(P.MonadicParser.regexp(re, group));
+}
+
+export function ofOp(name: string, short?: string) {
+    const op = short ? `${name}|${escapeRegExp(short)}` : name;
+    const re = RegExp(`(${op})\\s+([-+]?[0-9]*\\.?[0-9]+)\\s+OF`, 'i');
+    return infixOp(re, 2).map(parseFloat);
+}
+
+export function makeError(msg: string) {
+    return function () {
+        throw new Error(msg);
+    };
+}
+
+export function andExpr(selections: any[]) {
+    if (selections.length === 1) {
+        return selections[0];
+    } else if (selections.length > 1) {
+        return B.core.logic.and(selections);
+    } else {
+        return undefined;
+    }
+}
+
+export function orExpr(selections: any[]) {
+    if (selections.length === 1) {
+        return selections[0];
+    } else if (selections.length > 1) {
+        return B.core.logic.or(selections);
+    } else {
+        return undefined;
+    }
+}
+
+export function testExpr(property: any, args: any) {
+    if (args && args.op !== undefined && args.val !== undefined) {
+        const opArgs = [property, args.val];
+        switch (args.op) {
+            case '=': return B.core.rel.eq(opArgs);
+            case '!=': return B.core.rel.neq(opArgs);
+            case '>': return B.core.rel.gr(opArgs);
+            case '<': return B.core.rel.lt(opArgs);
+            case '>=': return B.core.rel.gre(opArgs);
+            case '<=': return B.core.rel.lte(opArgs);
+            default: throw new Error(`operator '${args.op}' not supported`);
+        }
+    } else if (args && args.flags !== undefined) {
+        return B.core.flags.hasAny([property, args.flags]);
+    } else if (args && args.min !== undefined && args.max !== undefined) {
+        return B.core.rel.inRange([property, args.min, args.max]);
+    } else if (!Array.isArray(args)) {
+        return B.core.rel.eq([property, args]);
+    } else if (args.length > 1) {
+        return B.core.set.has([B.core.type.set(args), property]);
+    } else {
+        return B.core.rel.eq([property, args[0]]);
+    }
+}
+
+export function invertExpr(selection: Expression) {
+    return B.struct.generator.queryInSelection({
+        0: selection, query: B.struct.generator.all(), 'in-complement': true }
+    );
+}
+
+export function strLenSortFn(a: string, b: string) {
+    return a.length < b.length ? 1 : -1;
+}
+
+function getNamesRegex(name: string, abbr?: string[]) {
+    const names = (abbr ? [name].concat(abbr) : [name])
+        .sort(strLenSortFn).map(escapeRegExp).join('|');
+    return RegExp(`${names}`, 'i');
+}
+
+export function getPropertyRules(properties: PropertyDict) {
+    // in keyof typeof properties
+    const propertiesDict: { [name: string]: P.MonadicParser<any> } = {};
+
+    Object.keys(properties).sort(strLenSortFn).forEach(name => {
+        const ps = properties[name];
+        const errorFn = makeError(`property '${name}' not supported`);
+        const rule = P.MonadicParser.regexp(ps.regex).map((x: any) => {
+            if (ps.isUnsupported) errorFn();
+            return testExpr(ps.property, ps.map(x));
+        });
+
+        if (!ps.isNumeric) {
+            propertiesDict[name] = rule;
+        }
+    });
+
+    return propertiesDict;
+}
+
+export function getNamedPropertyRules(properties: PropertyDict) {
+    const namedPropertiesList: P.MonadicParser<any>[] = [];
+
+    Object.keys(properties).sort(strLenSortFn).forEach(name => {
+        const ps = properties[name];
+        const errorFn = makeError(`property '${name}' not supported`);
+        const rule = P.MonadicParser.regexp(ps.regex).map((x: any) => {
+            if (ps.isUnsupported) errorFn();
+            return testExpr(ps.property, ps.map(x));
+        });
+        const nameRule = P.MonadicParser.regexp(getNamesRegex(name, ps.abbr)).trim(P.MonadicParser.optWhitespace);
+        const groupMap = (x: any) => B.struct.generator.atomGroups({ [ps.level]: x });
+
+        if (ps.isNumeric) {
+            namedPropertiesList.push(
+                nameRule.then(P.MonadicParser.seq(
+                    P.MonadicParser.regexp(/>=|<=|=|!=|>|</).trim(P.MonadicParser.optWhitespace),
+                    P.MonadicParser.regexp(ps.regex).map(ps.map)
+                )).map((x: any) => {
+                    if (ps.isUnsupported) errorFn();
+                    return testExpr(ps.property, { op: x[0], val: x[1] });
+                }).map(groupMap)
+            );
+        } else {
+            namedPropertiesList.push(nameRule.then(rule).map(groupMap));
+        }
+    });
+
+    return namedPropertiesList;
+}
+
+export function getKeywordRules(keywords: KeywordDict) {
+    const keywordsList: P.MonadicParser<any>[] = [];
+
+    Object.keys(keywords).sort(strLenSortFn).forEach(name => {
+        const ks = keywords[name];
+        const mapFn = ks.map ? ks.map : makeError(`keyword '${name}' not supported`);
+        const rule = P.MonadicParser.regexp(getNamesRegex(name, ks.abbr)).map(mapFn);
+        keywordsList.push(rule);
+    });
+
+    return keywordsList;
+}
+
+export function getFunctionRules(functions: FunctionDict, argRule: P.MonadicParser<any>) {
+    const functionsList: P.MonadicParser<any>[] = [];
+    const begRule = P.MonadicParser.regexp(/\(\s*/);
+    const endRule = P.MonadicParser.regexp(/\s*\)/);
+
+    Object.keys(functions).sort(strLenSortFn).forEach(name => {
+        const fs = functions[name];
+        const mapFn = fs.map ? fs.map : makeError(`function '${name}' not supported`);
+        const rule = P.MonadicParser.regexp(new RegExp(name, 'i')).skip(begRule).then(argRule).skip(endRule).map(mapFn);
+        functionsList.push(rule);
+    });
+
+    return functionsList;
+}
+
+export function getPropertyNameRules(properties: PropertyDict, lookahead: RegExp) {
+    const list: P.MonadicParser<any>[] = [];
+    Object.keys(properties).sort(strLenSortFn).forEach(name => {
+        const ps = properties[name];
+        const errorFn = makeError(`property '${name}' not supported`);
+        const rule = (P.MonadicParser as any).regexp(getNamesRegex(name, ps.abbr)).lookahead(lookahead).map(() => {
+            if (ps.isUnsupported) errorFn();
+            return ps.property;
+        });
+        list.push(rule);
+    });
+
+    return list;
+}
+
+export function getReservedWords(properties: PropertyDict, keywords: KeywordDict, operators: OperatorList, functions?: FunctionDict) {
+    const w: string[] = [];
+    for (const name in properties) {
+        w.push(name);
+        if (properties[name].abbr) w.push(...properties[name].abbr!);
+    }
+    for (const name in keywords) {
+        w.push(name);
+        if (keywords[name].abbr) w.push(...keywords[name].abbr!);
+    }
+    operators.forEach(o => {
+        w.push(o.name);
+        if (o.abbr) w.push(...o.abbr);
+    });
+    return w;
+}
+
+export function atomNameSet(ids: string[]) {
+    return B.core.type.set(ids.map(B.atomName));
+}
+
+export function asAtoms(e: Expression) {
+    return B.struct.generator.queryInSelection({
+        0: e,
+        query: B.struct.generator.all()
+    });
+}
+
+export function wrapValue(property: any, value: any, sstrucDict?: any) {
+    switch (property.head.name) {
+        case 'structure-query.atom-property.macromolecular.label_atom_id':
+            return B.atomName(value);
+        case 'structure-query.atom-property.core.element-symbol':
+            return B.es(value);
+        case 'structure-query.atom-property.macromolecular.secondary-structure-flags':
+            if (sstrucDict) {
+                value = [sstrucDict[value.toUpperCase()] || 'none'];
+            }
+            return B.struct.type.secondaryStructureFlags([value]);
+        default:
+            return value;
+    }
+}
+
+const propPrefix = 'structure-query.atom-property.macromolecular.';
+const entityProps = ['entityKey', 'label_entity_id', 'entityType'];
+const chainProps = ['chainKey', 'label_asym_id', 'label_entity_id', 'auth_asym_id', 'entityType'];
+const residueProps = ['residueKey', 'label_comp_id', 'label_seq_id', 'auth_comp_id', 'auth_seq_id', 'pdbx_formal_charge', 'secondaryStructureKey', 'secondaryStructureFlags', 'isModified', 'modifiedParentName'];
+export function testLevel(property: any) {
+    if (property.head.name.startsWith(propPrefix)) {
+        const name = property.head.name.substr(propPrefix.length);
+        if (entityProps.includes(name)) return 'entity-test';
+        if (chainProps.includes(name)) return 'chain-test';
+        if (residueProps.includes(name)) return 'residue-test';
+    }
+    return 'atom-test';
+}
+
+const flagProps = [
+    'structure-query.atom-property.macromolecular.secondary-structure-flags'
+];
+export function valuesTest(property: any, values: any[]) {
+    if (flagProps.includes(property.head.name)) {
+        const name = values[0].head;
+        const flags: any[] = [];
+        values.forEach(v => flags.push(...v.args[0]));
+        return B.core.flags.hasAny([property, { head: name, args: flags }]);
+    } else {
+        if (values.length === 1) {
+            return B.core.rel.eq([property, values[0]]);
+        } else if (values.length > 1) {
+            return B.core.set.has([B.core.type.set(values), property]);
+        }
+    }
+}
+
+export function resnameExpr(resnameList: string[]) {
+    return B.struct.generator.atomGroups({
+        'residue-test': B.core.set.has([
+            B.core.type.set(resnameList),
+            B.ammp('label_comp_id')
+        ])
+    });
+}
diff --git a/src/mol-script/transpilers/jmol/examples.ts b/src/mol-script/transpilers/jmol/examples.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e1fcac2892dc120472a9cea45197593ee3ff35af
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/examples.ts
@@ -0,0 +1,28 @@
+/**
+ * Copyright (c) 2017-2022 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>
+ *
+ * Adapted from MolQL project
+ */
+
+export const examples = [{
+    name: 'Residue 50 or 135',
+    value: '50 or 135'
+}, {
+    name: 'Atoms with no covalent bonds',
+    value: 'bondcount = 0'
+}, {
+    name: 'All 3-10 helices',
+    value: 'substructure = "helix310"'
+}, {
+    name: 'Metal atoms',
+    value: 'metal'
+}, {
+    name: 'Atoms invloved in aromatic bonds',
+    value: 'isAromatic'
+}, {
+    name: 'Pyrimidine residues',
+    value: 'pyrimidine'
+}];
diff --git a/src/mol-script/transpilers/jmol/keywords.ts b/src/mol-script/transpilers/jmol/keywords.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1dc545721b73a88691b89e1a84dc89011df8fab3
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/keywords.ts
@@ -0,0 +1,571 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * Adapted from MolQL project
+ */
+
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import * as h from '../helper';
+import { KeywordDict } from '../types';
+
+const ResDict = {
+    acidic: ['ASP', 'GLU'],
+    aliphatic: ['ALA', 'GLY', 'ILE', 'LEU', 'VAL'],
+    amino: ['ALA', 'ARG', 'ASN', 'ASP', 'CYS', 'GLN', 'GLU', 'GLY', 'HIS', 'ILE', 'LEU', 'LYS', 'MET', 'PHE', 'PRO', 'SER', 'THR', 'TRP', 'TYR', 'VAL', 'ASX', 'GLX', 'UNK'],
+    aromatic: ['HIS', 'PHE', 'TRP', 'TYR'],
+    basic: ['ARG', 'HIS', 'LYS'],
+    buried: ['ALA', 'CYS', 'ILE', 'LEU', 'MET', 'PHE', 'TRP', 'VAL'],
+    cg: ['CYT', 'C', 'GUA', 'G'],
+    cyclic: ['HIS', 'PHE', 'PRO', 'TRP', 'TYR'],
+    hydrophobic: ['ALA', 'GLY', 'ILE', 'LEU', 'MET', 'PHE', 'PRO', 'TRP', 'TYR', 'VAL'],
+    large: ['ARG', 'GLU', 'GLN', 'HIS', 'ILE', 'LEU', 'LYS', 'MET', 'PHE', 'TRP', 'TYR'],
+    medium: ['ASN', 'ASP', 'CYS', 'PRO', 'THR', 'VAL'],
+    small: ['ALA', 'GLY', 'SER'],
+
+    nucleic: ['G', 'C', 'A', 'T', 'U', 'I', 'DG', 'DC', 'DA', 'DT', 'DU', 'DI', '+G', '+C', '+A', '+T', '+U', '+I']
+};
+
+const Backbone = {
+    nucleic: ['P', "O3'", "O5'", "C5'", "C4'", "C3'", 'OP1', 'OP2', 'O3*', 'O5*', 'C5*', 'C4*', 'C3*',
+        "C2'", "C1'", "O4'", "O2'"],
+    protein: ['C', 'N', 'CA']
+};
+
+function nucleicExpr() {
+    return B.struct.combinator.merge([
+        B.struct.generator.atomGroups({
+            'residue-test': B.core.set.has([
+                B.set(...ResDict.nucleic),
+                B.ammp('label_comp_id')
+            ])
+        }),
+        B.struct.filter.pick({
+            0: B.struct.generator.atomGroups({
+                'group-by': B.ammp('residueKey')
+            }),
+            test: B.core.logic.and([
+                B.core.rel.eq([B.struct.atomSet.atomCount(), 1]),
+                B.core.rel.eq([B.ammp('label_atom_id'), B.atomName('P')]),
+            ])
+        }),
+        B.struct.filter.pick({
+            0: B.struct.generator.atomGroups({
+                'group-by': B.ammp('residueKey')
+            }),
+            test: B.core.logic.or([
+                B.core.set.isSubset([
+                    h.atomNameSet(["C1'", "C2'", "O3'", "C3'", "C4'", "C5'", "O5'"]),
+                    B.ammpSet('label_atom_id')
+                ]),
+                B.core.set.isSubset([
+                    h.atomNameSet(['C1*', 'C2*', 'O3*', 'C3*', 'C4*', 'C5*', 'O5*']),
+                    B.ammpSet('label_atom_id')
+                ])
+            ])
+        })
+    ]);
+}
+
+// TODO: improve, see keywords.protein['@desc'] below
+function proteinExpr() {
+    return B.struct.generator.atomGroups({
+        'residue-test': B.core.set.has([
+            B.set(...ResDict.amino),
+            B.ammp('label_comp_id')
+        ])
+    });
+}
+
+// TODO: improve, see keywords.backbone['@desc'] below
+function backboneExpr() {
+    return B.struct.combinator.merge([
+        B.struct.modifier.intersectBy({
+            0: B.struct.generator.atomGroups({
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.amino),
+                    B.ammp('label_comp_id')
+                ])
+            }),
+            by: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    B.core.type.set(Backbone.protein),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        }),
+        B.struct.modifier.intersectBy({
+            0: B.struct.generator.atomGroups({
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.nucleic),
+                    B.ammp('label_comp_id')
+                ])
+            }),
+            by: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    B.core.type.set(Backbone.nucleic),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        }),
+    ]);
+}
+
+export const keywords: KeywordDict = {
+    // general terms
+    all: {
+        '@desc': 'all atoms; same as *',
+        abbr: ['*'],
+        map: () => B.struct.generator.all()
+    },
+    bonded: {
+        '@desc': 'covalently bonded',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.gr([B.struct.atomProperty.core.bondCount({
+                flags: B.struct.type.bondFlags(['covalent', 'metallic', 'sulfide'])
+            }), 0])
+        })
+    },
+    clickable: {
+        '@desc': 'actually visible -- having some visible aspect such as wireframe, spacefill, or a label showing, or the alpha-carbon or phosphorus atom in a biomolecule that is rendered with only cartoon, rocket, or other biomolecule-specific shape.'
+    },
+    connected: {
+        '@desc': 'bonded in any way, including hydrogen bonds',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.gr([B.struct.atomProperty.core.bondCount({
+                flags: B.struct.type.bondFlags()
+            }), 0])
+        })
+    },
+    displayed: {
+        '@desc': 'displayed using the display or hide command; not necessarily visible'
+    },
+    hidden: {
+        '@desc': 'hidden using the display or hide command'
+    },
+    none: {
+        '@desc': 'no atoms',
+        map: () => B.struct.generator.empty()
+    },
+    selected: {
+        '@desc': 'atoms that have been selected; defaults to all when a file is first loaded'
+    },
+    thisModel: {
+        '@desc': 'atoms in the current frame set, as defined by frame, model, or animation commands. If more than one model is in this set, "thisModel" refers to all of them, regardless of atom displayed/hidden status.'
+    },
+    visible: {
+        '@desc': 'visible in any way, including PDB residue atoms for which a cartoon or other such rendering makes their group visible, even if they themselves are not visible.'
+    },
+    subset: {
+        '@desc': 'the currently defined subset. Note that if a subset is currently defined, then select/display all is the same as select/display subset, restrict none is the same as restrict not subset. In addition, select not subset selects nothing.'
+    },
+    specialPosition: {
+        '@desc': 'atoms in crystal structures that are at special positions - that is, for which there is more than one operator that leads to them.'
+    },
+    unitcell: {
+        '@desc': 'atoms within the current unitcell, which may be offset. This includes atoms on the faces and at the vertices of the unitcell.'
+    },
+    polyhedra: {
+        '@desc': 'all central atoms for which polyhedra have been created. See also polyhera(n), below. (Jmol 14.4)'
+    },
+    nonmetal: {
+        '@desc': '_H,_He,_B,_C,_N,_O,_F,_Ne,_Si,_P,_S,_Cl,_Ar,_As,_Se,_Br,_Kr,_Te,_I,_Xe,_At,_Rn',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.set.has([
+                B.set(...['H', 'He', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Si', 'P', 'S', 'Cl', 'Ar', 'As', 'Se', 'Br', 'Kr', 'Te', 'I', 'Xe', 'At', 'Rn'].map(B.es)),
+                B.acp('elementSymbol')
+            ])
+        })
+    },
+    metal: {
+        '@desc': '!nonmetal',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.logic.not([
+                B.core.set.has([
+                    B.set(...['H', 'He', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Si', 'P', 'S', 'Cl', 'Ar', 'As', 'Se', 'Br', 'Kr', 'Te', 'I', 'Xe', 'At', 'Rn'].map(B.es)),
+                    B.acp('elementSymbol')
+                ])
+            ])
+        })
+    },
+    alkaliMetal: {
+        '@desc': '_Li,_Na,_K,_Rb,_Cs,_Fr',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.set.has([
+                B.set(...['Li', 'Na', 'K', 'Rb', 'Cs', 'Fr'].map(B.es)),
+                B.acp('elementSymbol')
+            ])
+        })
+    },
+    alkalineEarth: {
+        '@desc': '_Be,_Mg,_Ca,_Sr,_Ba,_Ra',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.set.has([
+                B.set(...['Be', 'Mg', 'Ca', 'Sr', 'Ba', 'Ra'].map(B.es)),
+                B.acp('elementSymbol')
+            ])
+        })
+    },
+    nobleGas: {
+        '@desc': '_He,_Ne,_Ar,_Kr,_Xe,_Rn',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.set.has([
+                B.set(...['He', 'Ne', 'Ar', 'Kr', 'Xe', 'Rn'].map(B.es)),
+                B.acp('elementSymbol')
+            ])
+        })
+    },
+    metalloid: {
+        '@desc': '_B,_Si,_Ge,_As,_Sb,_Te',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.set.has([
+                B.set(...['B', 'Si', 'Ge', 'As', 'Sb', 'Te'].map(B.es)),
+                B.acp('elementSymbol')
+            ])
+        })
+    },
+    transitionMetal: {
+        '@desc': '(includes La and Ac) elemno>=21 and elemno<=30, elemno=57, elemno=89, elemno>=39 and elemno<=48, elemno>=72 and elemno<=80, elemno>=104 and elemno<=112',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.logic.or([
+                B.core.rel.inRange([B.acp('atomicNumber'), 21, 30]),
+                B.core.rel.inRange([B.acp('atomicNumber'), 39, 48]),
+                B.core.rel.inRange([B.acp('atomicNumber'), 72, 80]),
+                B.core.rel.inRange([B.acp('atomicNumber'), 104, 112]),
+                B.core.set.has([B.set(57, 89), B.acp('atomicNumber')])
+            ])
+        })
+    },
+    lanthanide: {
+        '@desc': '(does not include La) elemno>57 and elemno<=71',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.inRange([B.acp('atomicNumber'), 57, 71])
+        })
+    },
+    actinide: {
+        '@desc': '(does not include Ac) elemno>89 and elemno<=103',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.inRange([B.acp('atomicNumber'), 89, 103])
+        })
+    },
+    isaromatic: {
+        '@desc': 'atoms connected with the AROMATIC, AROMATICSINGLE, or AROMATICDOUBLE bond types',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.gr([
+                B.struct.atomProperty.core.bondCount({
+                    flags: B.struct.type.bondFlags(['aromatic'])
+                }),
+                0
+            ])
+        })
+    },
+
+    carbohydrate: {
+        '@desc': ''
+    },
+    ions: {
+        '@desc': '(specifically the PDB designations "PO4" and "SO4")'
+    },
+    ligand: {
+        '@desc': '(originally "hetero and not solvent"; changed to "!(protein,nucleic,water,UREA)" for Jmol 12.2)'
+    },
+    nucleic: {
+        '@desc': 'any group that (a) has one of the following group names: G, C, A, T, U, I, DG, DC, DA, DT, DU, DI, +G, +C, +A, +T, +U, +I; or (b) can be identified as a group that is only one atom, with name "P"; or (c) has all of the following atoms (prime, \', can replace * here): C1*, C2*, C3*, O3*, C4*, C5*, and O5*.',
+        map: () => nucleicExpr()
+    },
+    purine: {
+        '@desc': 'any nucleic group that (a) has one of the following group names: A, G, I, DA, DG, DI, +A, +G, or +I; or (b) also has atoms N7, C8, and N9.',
+        map: () => B.struct.modifier.intersectBy({
+            0: nucleicExpr(),
+            by: B.struct.combinator.merge([
+                B.struct.generator.atomGroups({
+                    'residue-test': B.core.set.has([
+                        B.set(...['A', 'G', 'I', 'DA', 'DG', 'DI', '+A', '+G', '+I']),
+                        B.ammp('label_comp_id')
+                    ])
+                }),
+                B.struct.filter.pick({
+                    0: B.struct.generator.atomGroups({
+                        'group-by': B.ammp('residueKey')
+                    }),
+                    test: B.core.set.isSubset([
+                        h.atomNameSet(['N7', 'C8', 'N9']),
+                        B.ammpSet('label_atom_id')
+                    ])
+                })
+            ])
+        })
+    },
+    pyrimidine: {
+        '@desc': 'any nucleic group that (a) has one of the following group names: C, T, U, DC, DT, DU, +C, +T, +U; or (b) also has atom O2.',
+        map: () => B.struct.modifier.intersectBy({
+            0: nucleicExpr(),
+            by: B.struct.combinator.merge([
+                B.struct.generator.atomGroups({
+                    'residue-test': B.core.set.has([
+                        B.set(...['C', 'T', 'U', 'DC', 'DT', 'DU', '+C', '+T', '+U']),
+                        B.ammp('label_comp_id')
+                    ])
+                }),
+                B.struct.filter.pick({
+                    0: B.struct.generator.atomGroups({
+                        'group-by': B.ammp('residueKey')
+                    }),
+                    test: B.core.logic.or([
+                        B.core.set.has([
+                            B.ammpSet('label_atom_id'),
+                            B.atomName('O2*')
+                        ]),
+                        B.core.set.has([
+                            B.ammpSet('label_atom_id'),
+                            B.atomName("O2'")
+                        ])
+                    ])
+                })
+            ])
+        })
+    },
+    dna: {
+        '@desc': 'any nucleic group that (a) has one of the following group names: DG, DC, DA, DT, DU, DI, T, +G, +C, +A, +T; or (b) has neither atom O2* or O2\'.',
+        map: () => B.struct.modifier.intersectBy({
+            0: nucleicExpr(),
+            by: B.struct.combinator.merge([
+                B.struct.generator.atomGroups({
+                    'residue-test': B.core.set.has([
+                        B.set(...['DG', 'DC', 'DA', 'DT', 'DU', 'DI', 'T', '+G', '+C', '+A', '+T']),
+                        B.ammp('label_comp_id')
+                    ])
+                }),
+                B.struct.filter.pick({
+                    0: B.struct.generator.atomGroups({
+                        'group-by': B.ammp('residueKey')
+                    }),
+                    test: B.core.logic.not([
+                        B.core.logic.or([
+                            B.core.set.has([
+                                B.ammpSet('label_atom_id'),
+                                B.atomName('O2*')
+                            ]),
+                            B.core.set.has([
+                                B.ammpSet('label_atom_id'),
+                                B.atomName("O2'")
+                            ])
+                        ])
+                    ])
+                })
+            ])
+        })
+    },
+    rna: {
+        '@desc': 'any nucleic group that (a) has one of the following group names: G, C, A, U, I, +U, +I; or (b) has atom O2* or O2\'.',
+        map: () => B.struct.modifier.intersectBy({
+            0: nucleicExpr(),
+            by: B.struct.combinator.merge([
+                B.struct.generator.atomGroups({
+                    'residue-test': B.core.set.has([
+                        B.set(...['G', 'C', 'A', 'U', 'I', '+U', '+I']),
+                        B.ammp('label_comp_id')
+                    ])
+                }),
+                B.struct.filter.pick({
+                    0: B.struct.generator.atomGroups({
+                        'group-by': B.ammp('residueKey')
+                    }),
+                    test: B.core.logic.or([
+                        B.core.set.has([
+                            B.ammpSet('label_atom_id'),
+                            B.atomName('O2*')
+                        ]),
+                        B.core.set.has([
+                            B.ammpSet('label_atom_id'),
+                            B.atomName("O2'")
+                        ])
+                    ])
+                })
+            ])
+        })
+    },
+    protein: {
+        '@desc': 'defined as a group that (a) has one of the following group names: ALA, ARG, ASN, ASP, CYS, GLN, GLU, GLY, HIS, ILE, LEU, LYS, MET, PHE, PRO, SER, THR, TRP, TYR, VAL, ASX, GLX, or UNK; or (b) contains PDB atom designations [C, O, CA, and N] bonded correctly; or (c) does not contain "O" but contains [C, CA, and N] bonded correctly; or (d) has only one atom, which has name CA and does not have the group name CA (indicating a calcium atom).',
+        map: () => proteinExpr()
+    },
+    acidic: {
+        '@desc': 'ASP GLU',
+        map: () => h.resnameExpr(ResDict.acidic)
+    },
+    acyclic: {
+        '@desc': 'amino and not cyclic',
+        map: () => B.struct.modifier.intersectBy({
+            0: h.resnameExpr(ResDict.amino),
+            by: h.invertExpr(h.resnameExpr(ResDict.cyclic))
+        })
+    },
+    aliphatic: {
+        '@desc': 'ALA GLY ILE LEU VAL',
+        map: () => h.resnameExpr(ResDict.aliphatic)
+    },
+    amino: {
+        '@desc': 'all twenty standard amino acids, plus ASX, GLX, UNK',
+        map: () => h.resnameExpr(ResDict.amino)
+    },
+    aromatic: {
+        '@desc': 'HIS PHE TRP TYR (see also "isaromatic" for aromatic bonds)',
+        map: () => h.resnameExpr(ResDict.aromatic)
+    },
+    basic: {
+        '@desc': 'ARG HIS LYS',
+        map: () => h.resnameExpr(ResDict.basic)
+    },
+    buried: {
+        '@desc': 'ALA CYS ILE LEU MET PHE TRP VAL',
+        map: () => h.resnameExpr(ResDict.buried)
+    },
+    charged: {
+        '@desc': 'same as acidic or basic -- ASP GLU, ARG HIS LYS',
+        map: () => h.resnameExpr(ResDict.acidic.concat(ResDict.basic))
+    },
+    cyclic: {
+        '@desc': 'HIS PHE PRO TRP TYR',
+        map: () => h.resnameExpr(ResDict.cyclic)
+    },
+    helix: {
+        '@desc': 'secondary structure-related.',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['helix']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    helixalpha: {
+        '@desc': 'secondary structure-related.',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['alpha']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    helix310: {
+        '@desc': 'secondary structure-related.',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['3-10']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    helixpi: {
+        '@desc': 'secondary structure-related.',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['pi']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    hetero: {
+        '@desc': 'PDB atoms designated as HETATM',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.ammp('isHet')
+        })
+    },
+    hydrophobic: {
+        '@desc': 'ALA GLY ILE LEU MET PHE PRO TRP TYR VAL',
+        map: () => h.resnameExpr(ResDict.hydrophobic)
+    },
+    large: {
+        '@desc': 'ARG GLU GLN HIS ILE LEU LYS MET PHE TRP TYR',
+        map: () => h.resnameExpr(ResDict.large)
+    },
+    medium: {
+        '@desc': 'ASN ASP CYS PRO THR VAL',
+        map: () => h.resnameExpr(ResDict.medium)
+    },
+    negative: {
+        '@desc': 'same as acidic -- ASP GLU',
+        map: () => h.resnameExpr(ResDict.acidic)
+    },
+    neutral: {
+        '@desc': 'amino and not (acidic or basic)',
+        map: () => B.struct.modifier.intersectBy({
+            0: h.resnameExpr(ResDict.amino),
+            by: h.invertExpr(h.resnameExpr(ResDict.acidic.concat(ResDict.basic)))
+        })
+    },
+    polar: {
+        '@desc': 'amino and not hydrophobic',
+        map: () => B.struct.modifier.intersectBy({
+            0: h.resnameExpr(ResDict.amino),
+            by: h.invertExpr(h.resnameExpr(ResDict.hydrophobic))
+        })
+    },
+    positive: {
+        '@desc': 'same as basic -- ARG HIS LYS',
+        map: () => h.resnameExpr(ResDict.basic)
+    },
+    sheet: {
+        '@desc': 'secondary structure-related',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['sheet']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    small: {
+        '@desc': 'ALA GLY SER',
+        map: () => h.resnameExpr(ResDict.small)
+    },
+    surface: {
+        '@desc': 'amino and not buried',
+        map: () => B.struct.modifier.intersectBy({
+            0: h.resnameExpr(ResDict.amino),
+            by: h.invertExpr(h.resnameExpr(ResDict.buried))
+        })
+    },
+    turn: {
+        '@desc': 'secondary structure-related',
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.flags.hasAny([
+                B.struct.type.secondaryStructureFlags(['turn']),
+                B.ammp('secondaryStructureFlags')
+            ])
+        })
+    },
+    alpha: {
+        '@desc': '(*.CA)',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.eq([
+                B.atomName('CA'),
+                B.ammp('label_atom_id')
+            ])
+        })
+    },
+    base: {
+        '@desc': '(nucleic bases)'
+    },
+    backbone: {
+        '@desc': '(*.C, *.CA, *.N, and all nucleic other than the bases themselves)',
+        abbr: ['mainchain'],
+        map: () => backboneExpr()
+    },
+    sidechain: {
+        '@desc': '((protein or nucleic) and not backbone)'
+    },
+    spine: {
+        '@desc': '(*.CA, *.N, *.C for proteins; *.P, *.O3\', *.O5\', *.C3\', *.C4\', *.C5 for nucleic acids)'
+    },
+    leadatom: {
+        '@desc': '(*.CA, *.P, and terminal *.O5\')'
+    },
+    solvent: {
+        '@desc': 'PDB "HOH", water, also the connected set of H-O-H in any model'
+    },
+};
+
+
diff --git a/src/mol-script/transpilers/jmol/markdown-docs.ts b/src/mol-script/transpilers/jmol/markdown-docs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15fd62c6de50ec23cd28b54c3ad629e5a2850377
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/markdown-docs.ts
@@ -0,0 +1,64 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+
+
+const _docs: string[] = [
+    'Jmol',
+    '============',
+    '--------------------------------',
+    ''
+];
+
+_docs.push(`## Properties\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+
+    const names = [name];
+    if (properties[name].abbr) names.push(...properties[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (properties[name]['@desc']) {
+        _docs.push(`*${properties[name]['@desc']}*\n`);
+    }
+}
+
+_docs.push(`## Operators\n\n`);
+_docs.push('--------------------------------\n');
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+
+    const names = [o.name];
+    if (o.abbr) names.push(...o.abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (o['@desc']) {
+        _docs.push(`*${o['@desc']}*\n`);
+    }
+});
+
+_docs.push(`## Keywords\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+
+    const names = [name];
+    if (keywords[name].abbr) names.push(...keywords[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (keywords[name]['@desc']) {
+        _docs.push(`*${keywords[name]['@desc']}*\n`);
+    }
+}
+
+export const docs = _docs.join('\n');
diff --git a/src/mol-script/transpilers/jmol/operators.ts b/src/mol-script/transpilers/jmol/operators.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ac9db6f007303ed416989fb0d243a30c555cc23e
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/operators.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { OperatorList } from '../types';
+
+export const operators: OperatorList = [
+    {
+        '@desc': 'Selects atoms that are not included in s1.',
+        '@examples': ['not ARG'],
+        name: 'not',
+        type: h.prefix,
+        rule: P.MonadicParser.alt(P.MonadicParser.regex(/NOT/i).skip(P.MonadicParser.whitespace), P.MonadicParser.string('!').skip(P.MonadicParser.optWhitespace)),
+        map: (op, selection) => h.invertExpr(selection),
+    },
+    {
+        '@desc': 'Selects atoms included in both s1 and s2.',
+        '@examples': ['ASP and .CA'],
+        name: 'and',
+        type: h.binaryLeft,
+        rule: h.infixOp(/AND|&/i),
+        map: (op, selection, by) => B.struct.modifier.intersectBy({ 0: selection, by })
+    },
+    {
+        '@desc': 'Selects atoms included in either s1 or s2.',
+        '@examples': ['ASP or GLU'],
+        name: 'or',
+        type: h.binaryLeft,
+        rule: h.infixOp(/OR|\||,/i),
+        map: (op, s1, s2) => B.struct.combinator.merge([s1, s2])
+    }
+];
+
diff --git a/src/mol-script/transpilers/jmol/parser.ts b/src/mol-script/transpilers/jmol/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8c9d0afc89b517b72f2c7260015b13d74bb95e65
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/parser.ts
@@ -0,0 +1,267 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Koya Sakuma < koya.sakuma.work@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { properties, structureMap } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+import { AtomGroupArgs } from '../types';
+import { Transpiler } from '../transpiler';
+import { OperatorList } from '../types';
+import { Expression } from '../../language/expression';
+
+// <, <=, =, >=, >, !=, and LIKE
+const valueOperators: OperatorList = [
+    {
+        '@desc': 'value comparisons',
+        '@examples': [],
+        name: '=',
+        abbr: ['=='],
+        type: h.binaryLeft,
+        rule: P.MonadicParser.regexp(/\s*(LIKE|>=|<=|=|!=|>|<)\s*/i, 1),
+        map: (op, e1, e2) => {
+            let expr;
+            if (e1 === 'structure') {
+                expr = B.core.flags.hasAny([B.ammp('secondaryStructureFlags'), structureMap(e2)]);
+            } else if (e2 === 'structure') {
+                expr = B.core.flags.hasAny([B.ammp('secondaryStructureFlags'), structureMap(e1)]);
+            } else if (e1.head !== undefined) {
+                if (e1.head.name === 'core.type.regex') {
+                    expr = B.core.str.match([e1, B.core.type.str([e2])]);
+                }
+            } else if (e2.head !== undefined) {
+                if (e2.head.name === 'core.type.regex') {
+                    expr = B.core.str.match([e2, B.core.type.str([e1])]);
+                }
+            } else if (op.toUpperCase() === 'LIKE') {
+                if (e1.head) {
+                    expr = B.core.str.match([
+                        B.core.type.regex([`^${e2}$`, 'i']),
+                        B.core.type.str([e1])
+                    ]);
+                } else {
+                    expr = B.core.str.match([
+                        B.core.type.regex([`^${e1}$`, 'i']),
+                        B.core.type.str([e2])
+                    ]);
+                }
+            }
+            if (!expr) {
+                if (e1.head) e2 = h.wrapValue(e1, e2);
+                if (e2.head) e1 = h.wrapValue(e2, e1);
+                switch (op) {
+                    case '=':
+                        expr = B.core.rel.eq([e1, e2]);
+                        break;
+                    case '!=':
+                        expr = B.core.rel.neq([e1, e2]);
+                        break;
+                    case '>':
+                        expr = B.core.rel.gr([e1, e2]);
+                        break;
+                    case '<':
+                        expr = B.core.rel.lt([e1, e2]);
+                        break;
+                    case '>=':
+                        expr = B.core.rel.gre([e1, e2]);
+                        break;
+                    case '<=':
+                        expr = B.core.rel.lte([e1, e2]);
+                        break;
+                    default: throw new Error(`value operator '${op}' not supported`);
+                }
+            }
+            return B.struct.generator.atomGroups({ 'atom-test': expr });
+        }
+    }
+];
+
+function atomExpressionQuery(x: any[]) {
+    const [resname, resnoRange, resno, inscode, chainname, atomname, altloc] = x[1];
+    const tests: AtomGroupArgs = {};
+
+    if (chainname) {
+        // TODO: should be configurable, there is an option in Jmol to use auth or label
+        tests['chain-test'] = B.core.rel.eq([B.ammp('auth_asym_id'), chainname]);
+    }
+
+    const resProps = [];
+    if (resname) resProps.push(B.core.rel.eq([B.ammp('label_comp_id'), resname]));
+    if (resnoRange) resProps.push(B.core.logic.and([
+        B.core.rel.gre([B.ammp('auth_seq_id'), resnoRange[0]]),
+        B.core.rel.lte([B.ammp('auth_seq_id'), resnoRange[1]])
+    ]));
+    if (resno) resProps.push(B.core.rel.eq([B.ammp('auth_seq_id'), resno]));
+    if (inscode) resProps.push(B.core.rel.eq([B.ammp('pdbx_PDB_ins_code'), inscode]));
+    if (resProps.length) tests['residue-test'] = h.andExpr(resProps);
+
+    const atomProps = [];
+    if (atomname) atomProps.push(B.core.rel.eq([B.ammp('auth_atom_id'), atomname]));
+    if (altloc) atomProps.push(B.core.rel.eq([B.ammp('label_alt_id'), altloc]));
+    if (atomProps.length) tests['atom-test'] = h.andExpr(atomProps);
+
+    return B.struct.generator.atomGroups(tests);
+}
+
+const lang = P.MonadicParser.createLanguage({
+    Integer: () => P.MonadicParser.regexp(/-?[0-9]+/).map(Number).desc('integer'),
+
+    Parens: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Parens,
+            r.Operator,
+            r.Expression
+        ).wrap(P.MonadicParser.regexp(/\(\s*/), P.MonadicParser.regexp(/\s*\)/));
+    },
+
+    Expression: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Keywords,
+
+            r.AtomExpression.map(atomExpressionQuery),
+
+            r.Within.map((x: [number, Expression]) => B.struct.modifier.includeSurroundings({ 0: x[1], radius: x[0] })),
+            r.ValueQuery,
+
+            r.Element.map((x: string) => B.struct.generator.atomGroups({
+                'atom-test': B.core.rel.eq([B.acp('elementSymbol'), B.struct.type.elementSymbol(x)])
+            })),
+            r.Resname.map((x: string) => B.struct.generator.atomGroups({
+                'residue-test': B.core.rel.eq([B.ammp('label_comp_id'), x])
+            })),
+        );
+    },
+
+    Operator: function (r: any) {
+        return h.combineOperators(operators, P.MonadicParser.alt(r.Parens, r.Expression));
+    },
+
+    AtomExpression: function (r: any) {
+        return P.MonadicParser.seq(
+            P.MonadicParser.lookahead(r.AtomPrefix),
+            P.MonadicParser.seq(
+                r.BracketedResname.or(P.MonadicParser.of(null)),
+                r.ResnoRange.or(P.MonadicParser.of(null)),
+                r.Resno.or(P.MonadicParser.of(null)),
+                r.Inscode.or(P.MonadicParser.of(null)),
+                r.Chainname.or(P.MonadicParser.of(null)),
+                r.Atomname.or(P.MonadicParser.of(null)),
+                r.Altloc.or(P.MonadicParser.of(null)),
+                r.Model.or(P.MonadicParser.of(null))
+            )
+        ).desc('expression');
+    },
+
+    AtomPrefix: () => P.MonadicParser.regexp(/[\[0-9:^%/.-]/).desc('atom-prefix'),
+
+    Chainname: () => P.MonadicParser.regexp(/:([A-Za-z]{1,3})/, 1).desc('chainname'),
+    Model: () => P.MonadicParser.regexp(/\/([0-9]+)/, 1).map(Number).desc('model'),
+    Element: () => P.MonadicParser.regexp(/_([A-Za-z]{1,3})/, 1).desc('element'),
+    Atomname: () => P.MonadicParser.regexp(/\.([a-zA-Z0-9]{1,4})/, 1).map(B.atomName).desc('atomname'),
+    Resname: () => P.MonadicParser.regexp(/[a-zA-Z0-9]{1,4}/).desc('resname'),
+    Resno: (r: any) => r.Integer.desc('resno'),
+    Altloc: () => P.MonadicParser.regexp(/%([a-zA-Z0-9])/, 1).desc('altloc'),
+    Inscode: () => P.MonadicParser.regexp(/\^([a-zA-Z0-9])/, 1).desc('inscode'),
+
+    BracketedResname: () => P.MonadicParser.regexp(/\[([a-zA-Z0-9]{1,4})\]/, 1).desc('bracketed-resname'),
+    ResnoRange: (r: any) => {
+        return P.MonadicParser.seq(
+            r.Integer.skip(P.MonadicParser.seq(
+                P.MonadicParser.optWhitespace,
+                P.MonadicParser.string('-'),
+                P.MonadicParser.optWhitespace
+            )),
+            r.Integer
+        ).desc('resno-range');
+    },
+    Within: (r: any) => {
+        return P.MonadicParser.regexp(/within/i)
+            .skip(P.MonadicParser.regexp(/\s*\(\s*/))
+            .then(P.MonadicParser.seq(
+                r.Integer.skip(P.MonadicParser.regexp(/\s*,\s*/)),
+                r.Query
+            ))
+            .skip(P.MonadicParser.regexp(/\)/));
+    },
+
+    Keywords: () => P.MonadicParser.alt(...h.getKeywordRules(keywords)).desc('keyword'),
+
+    Query: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Operator,
+            r.Parens,
+            r.Expression
+        ).trim(P.MonadicParser.optWhitespace);
+    },
+
+    Number: function () {
+        return P.MonadicParser.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
+            .map(Number)
+            .desc('number');
+    },
+
+    String: function () {
+        const w = h.getReservedWords(properties, keywords, operators)
+            .sort(h.strLenSortFn).map(h.escapeRegExp).join('|');
+        return P.MonadicParser.alt(
+            P.MonadicParser.regexp(new RegExp(`(?!(${w}))[A-Z0-9_]+`, 'i')),
+            P.MonadicParser.regexp(/'((?:[^"\\]|\\.)*)'/, 1),
+            P.MonadicParser.regexp(/"((?:[^"\\]|\\.)*)"/, 1).map(x => B.core.type.regex([`^${x}$`, 'i']))
+        ).desc('string');
+    },
+
+    Value: function (r: any) {
+        return P.MonadicParser.alt(r.Number, r.String);
+    },
+
+    ValueParens: function (r: any) {
+        return P.MonadicParser.alt(
+            r.ValueParens,
+            r.ValueOperator,
+            r.ValueExpressions
+        ).wrap(P.MonadicParser.string('('), P.MonadicParser.string(')'));
+    },
+
+    ValuePropertyNames: function () {
+        return P.MonadicParser.alt(...h.getPropertyNameRules(properties, /LIKE|>=|<=|=|!=|>|<|\)|\s/i));
+    },
+
+    ValueOperator: function (r: any) {
+        return h.combineOperators(valueOperators, P.MonadicParser.alt(r.ValueParens, r.ValueExpressions));
+    },
+
+    ValueExpressions: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Value,
+            r.ValuePropertyNames
+        );
+    },
+
+    ValueQuery: function (r: any) {
+        return P.MonadicParser.alt(
+            r.ValueOperator.map((x: any) => {
+                if (x.head) {
+                    if (x.head.name.startsWith('structure-query.generator')) return x;
+                } else {
+                    if (typeof x === 'string' && x.length <= 4) {
+                        return B.struct.generator.atomGroups({
+                            'residue-test': B.core.rel.eq([B.ammp('label_comp_id'), x])
+                        });
+                    }
+                }
+                throw new Error(`values must be part of an comparison, value '${x}'`);
+            })
+        );
+    }
+});
+
+export const transpiler: Transpiler = str => lang.Query.tryParse(str);
diff --git a/src/mol-script/transpilers/jmol/properties.ts b/src/mol-script/transpilers/jmol/properties.ts
new file mode 100644
index 0000000000000000000000000000000000000000..566e6780f9d016016a77c0d20c3bde6b21e80308
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/properties.ts
@@ -0,0 +1,667 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { PropertyDict } from '../types';
+
+const reFloat = /[-+]?[0-9]*\.?[0-9]+/;
+const rePosInt = /[0-9]+/;
+
+function str(x: string) { return x; }
+
+const structureDict: { [key: string]: string } = {
+    none: 'none',
+    turn: 'turn',
+    sheet: 'beta',
+    helix: 'helix',
+    dna: 'dna',
+    rna: 'rna',
+    carbohydrate: 'carbohydrate',
+    helix310: '3-10',
+    helixalpha: 'alpha',
+    helixpi: 'pi',
+
+    0: 'none',
+    1: 'turn',
+    2: 'beta',
+    3: 'helix',
+    4: 'dna',
+    5: 'rna',
+    6: 'carbohydrate',
+    7: '3-10',
+    8: 'alpha',
+    9: 'pi',
+};
+export function structureMap(x: any) {
+    if (x.head) {
+        if (x.head.name && x.head.name === 'core.type.regex') x = x.args[0].replace(/^\^|\$$/g, '');
+        x = structureDict[x.toString().toLowerCase()] || 'none';
+        if (['dna', 'rna', 'carbohydrate'].indexOf(x) !== -1) {
+            throw new Error("values 'dna', 'rna', 'carbohydrate' not yet supported for 'structure' property");
+        } else {
+            return B.struct.type.secondaryStructureFlags([x]);
+        }
+    }
+}
+
+export const properties: PropertyDict = {
+    adpmax: {
+        '@desc': 'the maximum anisotropic displacement parameter for the selected atom',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    adpmin: {
+        '@desc': 'the minimum anisotropic displacement parameter for the selected atom',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    altloc: {
+        '@desc': 'PDB alternate location identifier',
+        '@examples': ['altloc = A'],
+        regex: /[a-zA-Z0-9]/, map: str,
+        level: 'atom-test', property: B.ammp('label_alt_id')
+    },
+    altname: {
+        '@desc': 'an alternative name given to atoms by some file readers (for example, P2N)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[a-zA-Z0-9]/, map: str,
+        level: 'atom-test'
+    },
+    atomID: {
+        '@desc': 'special atom IDs for PDB atoms assigned by Jmol',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    atomIndex: {
+        '@desc': 'atom 0-based index; a unique number for each atom regardless of the number of models loaded',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    atomName: {
+        '@desc': 'atom name',
+        '@examples': ['atomName = CA'],
+        regex: /[a-zA-Z0-9]+/, map: v => B.atomName(v),
+        level: 'atom-test', property: B.ammp('label_atom_id')
+    },
+    atomno: {
+        '@desc': 'sequential number; you can use "@" instead of "atomno=" -- for example, select @33 or Var x = @33 or @35',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    atomType: {
+        '@desc': 'atom type (mol2, AMBER files) or atom name (other file types)',
+        '@examples': ['atomType = OH'],
+        regex: /[a-zA-Z0-9]+/, map: v => B.atomName(v),
+        level: 'atom-test', property: B.ammp('label_atom_id')
+    },
+    atomX: {
+        '@desc': 'Cartesian X coordinate (or just X)',
+        '@examples': ['x = 4.2'],
+        abbr: ['X'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('x')
+    },
+    atomY: {
+        '@desc': 'Cartesian Y coordinate (or just Y)',
+        '@examples': ['y < 42'],
+        abbr: ['Y'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('y')
+    },
+    atomZ: {
+        '@desc': 'Cartesian Z coordinate (or just Z)',
+        '@examples': ['Z > 10'],
+        abbr: ['Z'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('z')
+    },
+    bondcount: {
+        '@desc': 'covalent bond count',
+        '@examples': ['bondcount = 0'],
+        isNumeric: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test', property: B.acp('bondCount')
+    },
+    bondingRadius: {
+        '@desc': 'radius used for auto bonding; synonymous with ionic and ionicRadius',
+        '@examples': [''],
+        abbr: ['ionic', 'ionicRadius'],
+        isUnsupported: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    cell: {
+        '@desc': 'crystallographic unit cell, expressed either in lattice integer notation (111-999) or as a coordinate in ijk space, where {1 1 1} is the same as 555. ANDing two cells, for example select cell=555 and cell=556, selects the atoms on the common face. (Note: in the specifc case of CELL, only "=" is allowed as a comparator.)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    configuration: {
+        '@desc': 'Only in the context {configuration=n}, this option selects the set of atoms with either no ALTLOC specified or those atoms having this index into the array of altlocs within its model. So, for example, if the model has altloc "A" and "B", select configuration=1 is equivalent to select altloc="" or altloc="A", and print {configuration=2} is equivalent to print {altloc="" or altloc="B"}. Configuration 0 is "all atoms in a model having configurations", and an invalid configuration number gives no atoms. (Note: in the specifc case of CONFIGURATION, only "=" is allowed as a comparator.)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    chain: {
+        '@desc': 'protein chain. For newer CIF files allowing multicharacter chain specifications, use quotations marks: select chain="AA". For these multicharacter desigations, case is not checked unless the CIF file has lower-case chain designations.',
+        '@examples': ['chain = A', 'chain = "AA"'],
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'chain-test', property: B.ammp('auth_asym_id')
+    },
+    chainNo: {
+        '@desc': 'chain number; sequentially counted from 1 for each model; chainNo == 0 means"no chain" or PDB chain identifier indicated as a blank (Jmol 14.0).',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    color: {
+        '@desc': 'the atom color',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    covalentRadius: {
+        '@desc': 'covalent bonding radius, synonymous with covalent. Not used by Jmol, but could be used, for example, in {*}.spacefill={*}.covalentRadius.all.',
+        '@examples': [''],
+        abbr: ['covalent'],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    cs: {
+        '@desc': 'chemical shift calculated using computational results that include magnetic shielding tensors.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    element: {
+        '@desc': 'element symbol. The value of this parameter depends upon the context. Used with select structure=x, x can be either the quoted element symbol, "H", "He", "Li", etc. or atomic number. In all other contexts, the value is the element symbol. When the atom is a specific isotope, the string will contain the isotope number -- "13C", for example.',
+        '@examples': ['element=Fe'],
+        regex: /[a-zA-Z]+/, map: x => B.es(x),
+        level: 'atom-test', property: B.acp('elementSymbol')
+    },
+    elemno: {
+        '@desc': 'atomic element number',
+        '@examples': ['elemno=8'],
+        regex: /[0-9\s{}-]+/, map: x => parseInt(x),
+        level: 'atom-test', property: B.acp('atomicNumber')
+    },
+    eta: {
+        '@desc': 'Based on Carlos M. Duarte, Leven M. Wadley, and Anna Marie Pyle, RNA structure comparison, motif search and discovery using a reduced representation of RNA conformational space, Nucleic Acids Research, 2003, Vol. 31, No. 16 4755-4761. The parameter eta is the C4\'[i-1]-P[i]-C4\'[i]-P[i+1] dihedral angle; theta is the P[i]-C4\'[i]-P[i+1]-C4\'[i+1] dihedral angle. Both are measured on a 0-360 degree scale because they are commonly near 180 degrees. Using the commands plot PROPERTIES eta theta resno; select visible;wireframe only one can create these authors\' "RNA worm" graph.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    theta: {
+        '@desc': 'Based on Carlos M. Duarte, Leven M. Wadley, and Anna Marie Pyle, RNA structure comparison, motif search and discovery using a reduced representation of RNA conformational space, Nucleic Acids Research, 2003, Vol. 31, No. 16 4755-4761. The parameter eta is the C4\'[i-1]-P[i]-C4\'[i]-P[i+1] dihedral angle; theta is the P[i]-C4\'[i]-P[i+1]-C4\'[i+1] dihedral angle. Both are measured on a 0-360 degree scale because they are commonly near 180 degrees. Using the commands plot PROPERTIES eta theta resno; select visible;wireframe only one can create these authors\' "RNA worm" graph.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    file: {
+        '@desc': 'file number containing this atom',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    formalCharge: {
+        '@desc': 'formal charge',
+        '@examples': ['formalCharge=1'],
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('pdbx_formal_charge')
+    },
+    format: {
+        '@desc': 'format (label) of the atom.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fXyz: {
+        '@desc': 'fractional XYZ coordinates',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fX: {
+        '@desc': 'fractional X coordinate',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fY: {
+        '@desc': 'fractional Y coordinate',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fZ: {
+        '@desc': 'fractional Z coordinate',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fuxyz: {
+        '@desc': 'fractional XYZ coordinates in the unitcell coordinate system',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fux: {
+        '@desc': 'fractional X coordinate in the unitcell coordinate system',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fuy: {
+        '@desc': 'fractional Y coordinate in the unitcell coordinate system',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    fuz: {
+        '@desc': 'fractional Z coordinate in the unit cell coordinate system',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    group: {
+        '@desc': '3-letter residue code',
+        '@examples': ['group = ALA'],
+        regex: /[a-zA-Z0-9]{1,3}/, map: str,
+        level: 'residue-test', property: B.ammp('label_comp_id')
+    },
+    group1: {
+        '@desc': 'single-letter residue code (amino acids only)',
+        '@examples': ['group1 = G'],
+        regex: /[a-zA-Z]/, map: str,
+        level: 'residue-test', property: B.ammp('label_comp_id')
+    },
+    groupID: {
+        '@desc': 'group ID number: A unique ID for each amino acid or nucleic acid residue in a PDB file. 0  noGroup 1-5  ALA, ARG, ASN, ASP, CYS 6-10  GLN, GLU, GLY, HIS, ILE 11-15  LEU, LYS, MET, PHE, PRO 16-20  SER, THR, TRP, TYR, VAL 21-23  ASX, GLX, UNK 24-29  A, +A, G, +G, I, +I 30-35  C, +C, T, +T, U, +U Additional unique numbers are assigned arbitrarily by Jmol and cannot be used reproducibly.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    groupindex: {
+        '@desc': 'overall group index',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    hydrophobicity: {
+        '@desc': 'Aminoacid residue scale of hydrophobicity based on Rose, G. D., Geselowitz, A. R., Lesser, G. J., Lee, R. H., and Zehfus, M. H. (1985). Hydrophobicity of amino acid residues in globular proteins, Science, 229(4716):834-838.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    identify: {
+        '@desc': 'for a PDB/mmCIF file, a label such as [ILE]7^1:A.CD1%A/3 #47, which includes the group ([ILE]), residue number with optional insertion code (7^1), chain (:A), atom name (CD1), alternate location if present (%A), PDB model number (/3, for NMR models when one file is loaded; /file.model such as /2.3 if more than one file is loaded), and atom number (#47). For non-PDB data, the information is shorter -- for example, H15/2.1 #6, indicating atom name (H15), full file.model number (/2.1), and atom number (#6). If only a single model is loaded, %[identify] does not include the model number.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    insertion: {
+        '@desc': 'protein residue insertion code',
+        '@examples': ['insertion=A'],
+        regex: /[a-zA-Z0-9]/, map: str,
+        level: 'atom-test', property: B.ammp('pdbx_PDB_ins_code')
+    },
+    label: {
+        '@desc': 'current atom label (same as format)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    mass: {
+        '@desc': 'atomic mass -- especially useful with appended .max or .sum',
+        '@examples': ['mass > 13'],
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('mass')
+    },
+    model: {
+        '@desc': 'model number',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    modelindex: {
+        '@desc': 'a unique number for each model, starting with 0 and spanning all models in all files',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    modO: {
+        '@desc': 'currently calculated occupancy from modulation (0 to 100; NaN if atom has no occupancy modulation)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    modXYZ: {
+        '@desc': 'currently calculated displacement modulation (for incommensurately modulated structures). Also modX, modY, modZ for individual components. For atoms without modultion, {xx}.modXYZ is -1 and {xx}.modX is NaN, and in a label %[modXYZ] and %[modX] are blank.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    molecule: {
+        '@desc': 'molecule number',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    monomer: {
+        '@desc': 'monomer number (group number) in a polymer (usually a chain), starting with 1, or 0 if not part of a biopolymer -- that is, not a connected carbohydrate, amino acid, or nucleic acid (Jmol 14.3.15)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    ms: {
+        '@desc': 'magnetic shielding calculated from file-loaded tensors.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    occupancy: {
+        '@desc': 'CIF file site occupancy. In SELECT command comparisons ("select occupancy < 90"), an integer n implies measurement on a 0-100 scale; also, in the context %[occupancy] or %q for a label, the reported number is a percentage. In all other cases, such as when %Q is used in a label or when a decimal number is used in a comparison, the scale is 0.0 - 1.0.',
+        '@examples': ['occupancy < 1'],
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('occupancy')
+    },
+    partialCharge: {
+        '@desc': 'partial charge',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    phi: {
+        '@desc': 'protein group PHI angle for atom\'s residue',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    polymer: {
+        '@desc': 'sequential polymer number in a model, starting with 1.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    polymerLength: {
+        '@desc': 'polymer length',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    property_xx: {
+        '@desc': 'a property created using the DATA command',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    psi: {
+        '@desc': 'protein group PSI angle for the atom\'s residue',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    radius: {
+        '@desc': 'currently displayed radius -- In SELECT command comparisons ("select radius=n"), integer n implies Rasmol units 1/250 Angstroms; in all other cases or when a decimal number is used, the units are Angstroms.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    resno: {
+        '@desc': 'PDB residue number, not including insertion code (see also seqcode, below)',
+        '@examples': ['resno = 100'],
+        regex: /-?[0-9]+/, map: x => parseInt(x),
+        level: 'residue-test', property: B.ammp('auth_seq_id')
+    },
+    selected: {
+        '@desc': '1.0 if atom is selected; 0.0 if not',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    sequence: {
+        '@desc': 'PDB one-character sequence code, as a string of characters, with "?" indicated where single-character codes are not available',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    seqcode: {
+        '@desc': 'PDB residue number, including insertion code (for example, 234^2; "seqcode" option added in Jmol 14.3.16)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    seqid: {
+        '@desc': '(mmCIF only) the value from _atom_site.label_seq_id; a pointer to _entity_poly_seq.num in the ENTITY_POLY_SEQ category specifying the sequence of monomers in a polymer. Allowance is made for the possibility of microheterogeneity in a sample by allowing a given sequence number to be correlated with more than one monomer id. (Jmol 14.2.3)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    shape: {
+        '@desc': 'hybridization geometry such as "tetrahedral"',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    site: {
+        '@desc': 'crystallographic site number',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    spacefill: {
+        '@desc': 'currently displayed radius',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    straightness: {
+        '@desc': 'quaternion-derived straightness (second derivative of the quaternion describing the orientation of the residue. This quantity will have different values depending upon the setting of quaternionFrame as "A" (alpha-carbon/phosphorus atom only), "C" (alpha-carbon/pyrimidine or purine base based), "P" (carbonyl-carbon peptide plane/phosphorus tetrahedron based), or "N" (amide-nitrogen based). The default is alpha-carbon based, which corresponds closely to the following combination of Ramachandran angles involving three consecutive residues i-1, i, and i+1: -psii-1 - phii + psii + phii+1.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    strucno: {
+        '@desc': 'a unique number for each helix, sheet, or turn in a model, starting with 1.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    structure: {
+        '@desc': 'The value of this parameter depends upon the context. Used with select structure=x, x can be either the quoted keyword "none", "turn", "sheet", "helix", "dna", "rna", or "carbohydrate" or a respective number 0-6. In the context {*}.structure, the return value is a number; in the context label %[structure], the return is one of the six keywords.',
+        '@examples': ['structure="helix"', 'structure=3'],
+        regex: /none|turn|sheet|helix|dna|rna|carbohydrate|[0-6]/i, map: str,
+        level: 'residue-test', property: 'structure'
+    },
+    substructure: {
+        '@desc': 'like structure, the value of this parameter depends upon the context. Used with select substructure=x, x can be either the quoted keyword "none", "turn", "sheet", "helix", "dna", "rna", "carbohydrate", "helix310", "helixalpha", or "helixpi", or the respective number 0-9. In the context {*}.substructure, the return value is a number; in the context label %[substructure], the return is one of the nine keywords.',
+        '@examples': ['substructure = "alphahelix"', 'substructure =9'],
+        regex: /none|turn|sheet|helix|dna|rna|carbohydrate|helix310|helixalpha|helixpi|[0-9]/i, map: str,
+        level: 'residue-test', property: 'structure'
+    },
+    surfacedistance: {
+        '@desc': 'A value related to the distance of an atom to a nominal molecular surface. 0 indicates at the surface. Positive numbers are minimum distances in Angstroms from the given atom to the surface.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    symop: {
+        '@desc': 'the first symmetry operation code that generated this atom by Jmol; an integer starting with 1. See also symmetry, below. This operator is only present if the file contains space group information and the file was loaded using the {i, j, k} option so as to generate symmetry-based atoms. To select only the original atoms prior to application of symmetry, you can either use "SYMOP=n", where n is the symmetry operator corresponding to "x,y,z", or you can specify instead simply "NOT symmetry" the way you might specify "NOT hydrogen". Note that atoms in special positions will have multiple operator matches. These atoms can be selected using the keyword SPECIALPOSITION. The special form select SYMOP=nijk selects a specific translation of atoms from the given crystallographic symmetry operation. Comparators <, <=, >, >=, and != can be used and only apply to the ijk part of the designation. The ijk are relative, not absolute. Thus, symop=2555 selects for atoms that have been transformed by symop=2 but not subjected to any further translation. select symop=1555 is identical to select not symmetry. All other ijk are relative to these selections for 555. If the model was loaded using load "filename.cif" {444 666 1}, where the 1 indicates that all symmetry-generated atoms are to be packed within cell 555 and then translated to fill the other 26 specified cells, then select symop=3555 is nearly the same as select symop=3 and cell=555. (The difference being that cell=555 selects for all atoms that are on any edge of the cell, while symop=3555 does not.) However, the situation is different if instead the model was loaded using load "filename.cif" {444 666 0}, where the 0 indicates that symmetry-generated atoms are to be placed exactly where their symmetry operator would put them (x,-y,z being different then from x, 1-y, z). In that case, select symop=3555 is for all atoms that have been generated using symmetry operation 3 but have not had any additional translations applied to the x,y,z expression found in the CIF file. If, for example, symmetry operation 3 is -x,-y,-z, then load "filename.cif" {444 666 0} will place an atom originally at {1/2, 1/2, 1/2} at positions {-1/2, -1/2, -1/2} (symop=3555) and {-3/2, -3/2, -3/2} (symop=3444) and 24 other sites.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    symmetry: {
+        '@desc': 'as "symmetry" or in a label as lower-case "o" gives list of crystallographic symmetry operators generating this atom with lattice designations,such as 3555; upper-case "%O" in a label gives a list without the lattice designations. See also symop, above.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    temperature: {
+        '@desc': 'yes  yes  temperature factor (B-factor)',
+        '@examples': ['temperature >= 20'],
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('B_iso_or_equiv')
+    },
+    unitXyz: {
+        '@desc': 'unit cell XYZ coordinates',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    uX: {
+        '@desc': 'unit cell X coordinate normalized to [0,1)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    uY: {
+        '@desc': 'unit cell Y coordinate normalized to [0,1)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    uZ: {
+        '@desc': 'unit cell Z coordinate normalized to [0,1)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    valence: {
+        '@desc': 'the valence of an atom (sum of bonds, where double bond counts as 2 and triple bond counts as 3',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    vanderwaals: {
+        '@desc': 'van der Waals radius',
+        '@examples': ['vanderwaals >2'],
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('vdw')
+    },
+    vectorScale: {
+        '@desc': 'vibration vector scale',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    volume: {
+        '@desc': 'approximate van der Waals volume for this atom. Note, {*}.volume gives an average; use {*}.volume.sum to get total volume.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    vXyz: {
+        '@desc': 'vibration vector, or individual components as %vx %vy %vz. For atoms without vibration vectors, {xx}.vXyz is -1; in a label, %[vxyz] is blank.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    vX: {
+        '@desc': 'vibration vector X coordinate; for atoms without vibration vector, {xx}.vX is NaN (same for vY and vZ)',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    vY: {
+        '@desc': 'vibration vector Y coordinate',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    vZ: {
+        '@desc': 'vibration vector Z coordinate',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+    xyz: {
+        '@desc': 'Cartesian XYZ coordinates; select xyz > 1.0 selects atoms more than one Angstrom from the origin.',
+        '@examples': [''],
+        isUnsupported: true,
+        regex: /[0-9\s{}-]+/, map: str,
+        level: 'atom-test'
+    },
+};
+
diff --git a/src/mol-script/transpilers/jmol/symbols.ts b/src/mol-script/transpilers/jmol/symbols.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6720acb588f1cf2d00efd09f8612e7b0d212e56e
--- /dev/null
+++ b/src/mol-script/transpilers/jmol/symbols.ts
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+
+export const Properties: string[] = [];
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+    Properties.push(name);
+    if (properties[name].abbr) Properties.push(...properties[name].abbr!);
+}
+
+export const Operators: string[] = [];
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+    Operators.push(o.name);
+    if (o.abbr) Operators.push(...o.abbr);
+});
+
+export const Keywords: string[] = [];
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+    Keywords.push(name);
+    if (keywords[name].abbr) Keywords.push(...keywords[name].abbr!);
+}
+
+export const _all = { Properties, Operators, Keywords };
diff --git a/src/mol-script/transpilers/pymol/examples.ts b/src/mol-script/transpilers/pymol/examples.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c92105904b054ebf99667cd8c6bb05f706bf412b
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/examples.ts
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2017-2022 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 Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+export const examples = [{
+    name: 'ALA residues',
+    value: 'resn ALA'
+}, {
+    name: 'Atoms named "C", "O", "N", or "CA"',
+    value: 'name c+o+n+ca'
+}, {
+    name: 'Residues with helix or sheet secondary structure',
+    value: 'ss h+s'
+}, {
+    name: 'C-alpha atoms of residues 100 to 180 in chain A',
+    value: 'A/100-180/CA'
+}, {
+    name: 'Residues 100 to 180',
+    value: 'resi 100-180'
+}, {
+    name: 'Atoms that are 1 ang + vdw radius away from polymer',
+    value: 'polymer gap 1'
+}, {
+    name: 'Residues within 4 ang of HEM',
+    value: 'byres resn HEM around 4'
+}, {
+    name: 'HEM and residues within 4 ang',
+    value: 'byres resn HEM expand 4'
+}, {
+    name: 'Solvent close (2.5 ang) to polymer',
+    value: 'solvent NEAR_TO 2.5 OF polymer'
+}, {
+    name: 'Cystein residues within 3 ang of HEM',
+    value: 'byres resn CYS WITHIN 3 OF resn HEM'
+}, {
+    name: 'Solvent atoms 4 ang away from oxygen',
+    value: 'solvent beyond 4 of (name O and not solvent)'
+}, {
+    name: 'All rings in PHE',
+    value: 'byring resn PHE'
+}, {
+    name: 'CYS and all bound residues',
+    value: 'byres BOUND_TO resn CYS'
+}, {
+    name: 'HEM and atoms up to 7 bonds away',
+    value: 'resn HEM extend 7'
+}, {
+    name: 'Atoms with alternate location A or none',
+    value: 'alt A+""'
+}];
diff --git a/src/mol-script/transpilers/pymol/keywords.ts b/src/mol-script/transpilers/pymol/keywords.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1e3c95107e26a6dadfe340d0b97560b257907e3c
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/keywords.ts
@@ -0,0 +1,275 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import * as h from '../helper';
+import { KeywordDict } from '../types';
+
+const ResDict = {
+    nucleic: ['A', 'C', 'T', 'G', 'U', 'DA', 'DC', 'DT', 'DG', 'DU'],
+    protein: ['ALA', 'ARG', 'ASN', 'ASP', 'CYS', 'CYX', 'GLN', 'GLU', 'GLY', 'HIS', 'HID', 'HIE', 'HIP', 'ILE', 'LEU', 'LYS', 'MET', 'MSE', 'PHE', 'PRO', 'SER', 'THR', 'TRP', 'TYR', 'VAL'],
+    solvent: ['HOH', 'WAT', 'H20', 'TIP', 'SOL']
+};
+
+const Backbone = {
+    nucleic: ['P', "O3'", "O5'", "C5'", "C4'", "C3'", 'OP1', 'OP2', 'O3*', 'O5*', 'C5*', 'C4*', 'C3*',
+        "C2'", "C1'", "O4'", "O2'"],
+    protein: ['C', 'N', 'CA', 'O']
+};
+
+function backboneExpr() {
+    return B.struct.combinator.merge([
+        B.struct.modifier.intersectBy({
+            0: B.struct.generator.atomGroups({
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.protein),
+                    B.ammp('label_comp_id')
+                ])
+            }),
+            by: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    B.core.type.set(Backbone.protein),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        }),
+        B.struct.modifier.intersectBy({
+            0: B.struct.generator.atomGroups({
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.nucleic),
+                    B.ammp('label_comp_id')
+                ])
+            }),
+            by: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    B.core.type.set(Backbone.nucleic),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        }),
+    ]);
+}
+
+export const keywords: KeywordDict = {
+    all: {
+        '@desc': 'All atoms currently loaded into PyMOL',
+        abbr: ['*'],
+        map: () => B.struct.generator.all()
+    },
+    none: {
+        '@desc': 'No atoms (empty selection)',
+        map: () => B.struct.generator.empty()
+    },
+    hydrogens: {
+        '@desc': 'All hydrogen atoms currently loaded into PyMOL',
+        abbr: ['hydro', 'h.'],
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.eq([
+                B.acp('elementSymbol'),
+                B.es('H')
+            ])
+        })
+    },
+    hetatm: {
+        '@desc': 'All atoms loaded from Protein Data Bank HETATM records',
+        abbr: ['het'],
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.eq([B.ammp('isHet'), true])
+        })
+    },
+    visible: {
+        '@desc': 'All atoms in enabled objects with at least one visible representation',
+        abbr: ['v.']
+    },
+    polymer: {
+        '@desc': 'All atoms on the polymer (not het). Finds atoms with residue identifiers matching a known polymer, such a peptide and DNA.',
+        abbr: ['pol.'],
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.set.has([
+                B.core.type.set(ResDict.nucleic.concat(ResDict.protein)),
+                B.ammp('label_comp_id')
+            ])
+        })
+    },
+    sidechain: {
+        '@desc': 'Polymer non-backbone atoms (new in PyMOL 1.6.1)',
+        abbr: ['sc.'],
+        map: () => {
+            return B.struct.modifier.exceptBy({
+                '0': B.struct.generator.atomGroups({
+                    'residue-test': B.core.set.has([
+                        B.core.type.set(ResDict.nucleic.concat(ResDict.protein)),
+                        B.ammp('label_comp_id')
+                    ])
+                }),
+                by: backboneExpr()
+            });
+        },
+    },
+    present: {
+        '@desc': 'All atoms with defined coordinates in the current state (used in creating movies)',
+        abbr: ['pr.']
+    },
+    center: {
+        '@desc': 'Pseudo-atom at the center of the scene'
+    },
+    origin: {
+        '@desc': 'Pseudo-atom at the origin of rotation',
+    },
+    enabled: {
+        '@desc': 'All enabled objects or selections from the object list.',
+    },
+    masked: {
+        '@desc': 'All masked atoms.',
+        abbr: ['msk.']
+    },
+    protected: {
+        '@desc': 'All protected atoms.',
+        abbr: ['pr.']
+    },
+    bonded: {
+        '@desc': 'All bonded atoms',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.rel.gr([B.struct.atomProperty.core.bondCount({
+                flags: B.struct.type.bondFlags(['covalent', 'metallic', 'sulfide'])
+            }), 0])
+        })
+    },
+    donors: {
+        '@desc': 'All hydrogen bond donor atoms.',
+        abbr: ['don.']
+    },
+    acceptors: {
+        '@desc': 'All hydrogen bond acceptor atoms.',
+        abbr: ['acc.']
+    },
+    fixed: {
+        '@desc': 'All fixed atoms.',
+        abbr: ['fxd.']
+    },
+    restrained: {
+        '@desc': 'All restrained atoms.',
+        abbr: ['rst.']
+    },
+    organic: {
+        '@desc': 'All atoms in non-polymer organic compounds (e.g. ligands, buffers). Finds carbon-containing molecules that do not match known polymers.',
+        abbr: ['org.'],
+        map: () => h.asAtoms(B.struct.modifier.expandProperty({
+            '0': B.struct.modifier.union([
+                B.struct.generator.queryInSelection({
+                    '0': B.struct.generator.atomGroups({
+                        'residue-test': B.core.logic.not([
+                            B.core.set.has([
+                                B.core.type.set(ResDict.nucleic.concat(ResDict.protein)),
+                                B.ammp('label_comp_id')
+                            ])
+                        ])
+                    }),
+                    query: B.struct.generator.atomGroups({
+                        'atom-test': B.core.rel.eq([
+                            B.es('C'),
+                            B.acp('elementSymbol')
+                        ])
+                    })
+                })
+            ]),
+            property: B.ammp('residueKey')
+        }))
+    },
+    inorganic: {
+        '@desc': 'All non-polymer inorganic atoms/ions. Finds atoms in molecules that do not contain carbon and do not match any known solvent residues.',
+        abbr: ['ino.'],
+        map: () => h.asAtoms(B.struct.modifier.expandProperty({
+            '0': B.struct.modifier.union([
+                B.struct.filter.pick({
+                    '0': B.struct.generator.atomGroups({
+                        'residue-test': B.core.logic.not([
+                            B.core.set.has([
+                                B.core.type.set(ResDict.nucleic.concat(ResDict.protein).concat(ResDict.solvent)),
+                                B.ammp('label_comp_id')
+                            ])
+                        ]),
+                        'group-by': B.ammp('residueKey')
+                    }),
+                    test: B.core.logic.not([
+                        B.core.set.has([
+                            B.struct.atomSet.propertySet([B.acp('elementSymbol')]),
+                            B.es('C')
+                        ])
+                    ])
+                })
+            ]),
+            property: B.ammp('residueKey')
+        }))
+    },
+    solvent: {
+        '@desc': 'All water molecules. The hardcoded solvent residue identifiers are currently: HOH, WAT, H20, TIP, SOL.',
+        abbr: ['sol.'],
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.set.has([
+                B.core.type.set(ResDict.solvent),
+                B.ammp('label_comp_id')
+            ])
+        })
+    },
+    guide: {
+        '@desc': 'All protein CA and nucleic acid C4*/C4',
+        map: () => B.struct.combinator.merge([
+            B.struct.generator.atomGroups({
+                'atom-test': B.core.rel.eq([
+                    B.atomName('CA'),
+                    B.ammp('label_atom_id')
+                ]),
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.protein),
+                    B.ammp('label_comp_id')
+                ])
+            }),
+            B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    h.atomNameSet(['C4*', 'C4\'']),
+                    B.ammp('label_atom_id')
+                ]),
+                'residue-test': B.core.set.has([
+                    B.core.type.set(ResDict.nucleic),
+                    B.ammp('label_comp_id')
+                ])
+            })
+        ]),
+    },
+    metals: {
+        '@desc': 'All metal atoms (new in PyMOL 1.6.1)'
+    },
+    backbone: {
+        '@desc': 'Polymer backbone atoms (new in PyMOL 1.6.1)',
+        abbr: ['bb.'],
+        map: () => backboneExpr()
+    },
+    'polymer.protein': {
+        '@desc': 'Protein (New in PyMOL 2.1)',
+        abbr: ['polymer.protein'],
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.set.has([
+                B.core.type.set(ResDict.protein),
+                B.ammp('label_comp_id')
+            ])
+        })
+    },
+    'polymer.nucleic': {
+        '@desc': 'Nucleic Acid (New in PyMOL 2.1)',
+        abbr: ['polymer.nucleic'],
+        map: () => B.struct.generator.atomGroups({
+            'residue-test': B.core.set.has([
+                B.core.type.set(ResDict.nucleic),
+                B.ammp('label_comp_id')
+            ])
+        })
+    }
+};
diff --git a/src/mol-script/transpilers/pymol/markdown-docs.ts b/src/mol-script/transpilers/pymol/markdown-docs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0740d545ea21ace645261db85b9922f5b76633fb
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/markdown-docs.ts
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+
+const _docs: string[] = [
+    'PyMol',
+    '============',
+    '--------------------------------',
+    ''
+];
+
+_docs.push(`## Properties\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+
+    const names = [name];
+    if (properties[name].abbr) names.push(...properties[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (properties[name]['@desc']) {
+        _docs.push(`*${properties[name]['@desc']}*\n`);
+    }
+}
+
+_docs.push(`## Operators\n\n`);
+_docs.push('--------------------------------\n');
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+
+    const names = [o.name];
+    if (o.abbr) names.push(...o.abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (o['@desc']) {
+        _docs.push(`*${o['@desc']}*\n`);
+    }
+});
+
+_docs.push(`## Keywords\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+
+    const names = [name];
+    if (keywords[name].abbr) names.push(...keywords[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (keywords[name]['@desc']) {
+        _docs.push(`*${keywords[name]['@desc']}*\n`);
+    }
+}
+
+export const docs = _docs.join('\n');
\ No newline at end of file
diff --git a/src/mol-script/transpilers/pymol/operators.ts b/src/mol-script/transpilers/pymol/operators.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f1537e3e570788b9a38ecc2580a362ee1f849c3c
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/operators.ts
@@ -0,0 +1,371 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { OperatorList } from '../types';
+import { Expression } from '../../language/expression';
+
+export const operators: OperatorList = [
+    {
+        '@desc': 'Selects atoms that are not included in s1.',
+        '@examples': [
+            'NOT resn ALA',
+            'not (resi 42 or chain A)',
+            '!resi 42 or chain A',
+        ],
+        name: 'not',
+        type: h.prefix,
+        rule: P.MonadicParser.alt(
+            P.MonadicParser.regexp(/NOT/i).skip(P.MonadicParser.whitespace),
+            P.MonadicParser.string('!').skip(P.MonadicParser.optWhitespace)
+        ),
+        map: (op, selection) => h.invertExpr(selection),
+    },
+    {
+        '@desc': 'Selects atoms included in both s1 and s2.',
+        '@examples': ['chain A AND name CA'],
+        name: 'and',
+        type: h.binaryLeft,
+        rule: h.infixOp(/AND|&/i),
+        map: (op, selection, by) =>
+            B.struct.modifier.intersectBy({ 0: selection, by }),
+    },
+    {
+        '@desc': 'Selects atoms included in either s1 or s2.',
+        '@examples': ['chain A OR chain B'],
+        name: 'or',
+        type: h.binaryLeft,
+        rule: h.infixOp(/OR|\|/i),
+        map: (op: string, s1: Expression, s2: Expression) => B.struct.combinator.merge([s1, s2]),
+    },
+    {
+        '@desc':
+            'Selects atoms in s1 whose identifiers name, resi, resn, chain and segi all match atoms in s2.',
+        '@examples': ['chain A IN chain B'],
+        name: 'in',
+        type: h.binaryLeft,
+        rule: h.infixOp(/IN/i),
+        map: (op: string, selection: Expression, source: Expression) => {
+            return B.struct.filter.withSameAtomProperties({
+                0: selection,
+                source,
+                property: B.core.type.compositeKey([
+                    B.ammp('label_atom_id'),
+                    B.ammp('label_seq_id'),
+                    B.ammp('label_comp_id'),
+                    B.ammp('auth_asym_id'),
+                    B.ammp('label_asym_id'),
+                ]),
+            });
+        },
+    },
+    {
+        '@desc':
+            'Selects atoms in s1 whose identifiers name and resi match atoms in s2.',
+        '@examples': ['chain A LIKE chain B'],
+        name: 'like',
+        type: h.binaryLeft,
+        rule: h.infixOp(/LIKE|l\./i),
+        map: (op: string, selection: Expression, source: Expression) => {
+            return B.struct.filter.withSameAtomProperties({
+                0: selection,
+                source,
+                property: B.core.type.compositeKey([
+                    B.ammp('label_atom_id'),
+                    B.ammp('label_seq_id'),
+                ]),
+            });
+        },
+    },
+    {
+        '@desc':
+            'Selects all atoms whose van der Waals radii are separated from the van der Waals radii of s1 by a minimum of X Angstroms.',
+        '@examples': ['solvent GAP 2'],
+        name: 'gap',
+        type: h.postfix,
+        rule: h
+            .postfixOp(/GAP\s+([-+]?[0-9]*\.?[0-9]+)/i, 1)
+            .map((x: any) => parseFloat(x)),
+        map: (distance: number, target: Expression) => {
+            return B.struct.filter.within({
+                '0': B.struct.generator.all(),
+                target,
+                'atom-radius': B.acp('vdw'),
+                'max-radius': distance,
+                invert: true,
+            });
+        },
+    },
+    {
+        '@desc':
+            'Selects atoms with centers within X Angstroms of the center of any atom in s1.',
+        '@examples': ['resname LIG AROUND 1'],
+        name: 'around',
+        abbr: ['a.'],
+        type: h.postfix,
+        rule: h
+            .postfixOp(/(AROUND|a\.)\s+([-+]?[0-9]*\.?[0-9]+)/i, 2)
+            .map((x: any) => parseFloat(x)),
+        map: (radius: number, target: Expression) => {
+            return B.struct.modifier.exceptBy({
+                '0': B.struct.filter.within({
+                    '0': B.struct.generator.all(),
+                    target,
+                    'max-radius': radius,
+                }),
+                by: target,
+            });
+        },
+    },
+    {
+        '@desc':
+            'Expands s1 by all atoms within X Angstroms of the center of any atom in s1.',
+        '@examples': ['chain A EXPAND 3'],
+        name: 'expand',
+        abbr: ['x.'],
+        type: h.postfix,
+        rule: h
+            .postfixOp(/(EXPAND|x\.)\s+([-+]?[0-9]*\.?[0-9]+)/i, 2)
+            .map((x: any) => parseFloat(x)),
+        map: (radius: number, selection: Expression) => {
+            return B.struct.modifier.includeSurroundings({ 0: selection, radius });
+        },
+    },
+    {
+        '@desc':
+            'Selects atoms in s1 that are within X Angstroms of any atom in s2.',
+        '@examples': ['chain A WITHIN 3 OF chain B'],
+        name: 'within',
+        abbr: ['w.'],
+        type: h.binaryLeft,
+        rule: h.ofOp('WITHIN', 'w.'),
+        map: (radius: number, selection: Expression, target: Expression) => {
+            return B.struct.filter.within({
+                0: selection,
+                target,
+                'max-radius': radius,
+            });
+        },
+    },
+    {
+        '@desc':
+            'Same as within, but excludes s2 from the selection (and thus is identical to s1 and s2 around X).',
+        '@examples': ['chain A NEAR_TO 3 OF chain B'],
+        name: 'near_to',
+        abbr: ['nto.'],
+        type: h.binaryLeft,
+        rule: h.ofOp('NEAR_TO', 'nto.'),
+        map: (radius: number, selection: Expression, target: Expression) => {
+            return B.struct.modifier.exceptBy({
+                '0': B.struct.filter.within({
+                    '0': selection,
+                    target,
+                    'max-radius': radius,
+                }),
+                by: target,
+            });
+        },
+    },
+    {
+        '@desc': 'Selects atoms in s1 that are at least X Anstroms away from s2.',
+        '@examples': ['solvent BEYOND 2 OF chain A'],
+        name: 'beyond',
+        abbr: ['be.'],
+        type: h.binaryLeft,
+        rule: h.ofOp('BEYOND', 'be.'),
+        map: (radius: number, selection: Expression, target: Expression) => {
+            return B.struct.modifier.exceptBy({
+                '0': B.struct.filter.within({
+                    '0': selection,
+                    target,
+                    'max-radius': radius,
+                    invert: true,
+                }),
+                by: target,
+            });
+        },
+    },
+    {
+        '@desc': 'Expands selection to complete residues.',
+        '@examples': ['BYRESIDUE name N'],
+        name: 'byresidue',
+        abbr: ['byresi', 'byres', 'br.'],
+        type: h.prefix,
+        rule: h.prefixOp(/BYRESIDUE|byresi|byres|br\./i),
+        map: (op: string, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.expandProperty({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                    property: B.ammp('residueKey'),
+                })
+            );
+        },
+    },
+    {
+        '@desc':
+            'Completely selects all alpha carbons in all residues covered by a selection.',
+        '@examples': ['BYCALPHA chain A'],
+        name: 'bycalpha',
+        abbr: ['bca.'],
+        type: h.prefix,
+        rule: h.prefixOp(/BYCALPHA|bca\./i),
+        map: (op: string, selection: Expression) => {
+            return B.struct.generator.queryInSelection({
+                '0': B.struct.modifier.expandProperty({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                    property: B.ammp('residueKey'),
+                }),
+                query: B.struct.generator.atomGroups({
+                    'atom-test': B.core.rel.eq([
+                        B.atomName('CA'),
+                        B.ammp('label_atom_id'),
+                    ]),
+                }),
+            });
+        },
+    },
+    {
+        '@desc': 'Expands selection to complete molecules.',
+        '@examples': ['BYMOLECULE resi 20-30'],
+        name: 'bymolecule',
+        isUnsupported: true, // structure-query.atom-property.topology.connected-component-key' is not implemented
+        abbr: ['bymol', 'bm.'],
+        type: h.prefix,
+        rule: h.prefixOp(/BYMOLECULE|bymol|bm\./i),
+        map: (op: string, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.expandProperty({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                    property: B.atp('connectedComponentKey'),
+                })
+            );
+        },
+    },
+    {
+        '@desc': 'Expands selection to complete fragments.',
+        '@examples': ['BYFRAGMENT resi 10'],
+        name: 'byfragment',
+        abbr: ['byfrag', 'bf.'],
+        isUnsupported: true,
+        type: h.prefix,
+        rule: h.prefixOp(/BYFRAGMENT|byfrag|bf\./i),
+        map: (op: string, selection: Expression) => [op, selection],
+    },
+    {
+        '@desc': 'Expands selection to complete segments.',
+        '@examples': ['BYSEGMENT resn CYS'],
+        name: 'bysegment',
+        abbr: ['bysegi', 'byseg', 'bs.'],
+        type: h.prefix,
+        rule: h.prefixOp(/BYSEGMENT|bysegi|byseg|bs\./i),
+        map: (op: string, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.expandProperty({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                    property: B.ammp('chainKey'),
+                })
+            );
+        },
+    },
+    {
+        '@desc': 'Expands selection to complete objects.',
+        '@examples': ['BYOBJECT chain A'],
+        name: 'byobject',
+        abbr: ['byobj', 'bo.'],
+        isUnsupported: true,
+        type: h.prefix,
+        rule: h.prefixOp(/BYOBJECT|byobj|bo\./i),
+        map: (op: string, selection: Expression) => [op, selection],
+    },
+    {
+        '@desc': 'Expands selection to unit cell.',
+        '@examples': ['BYCELL chain A'],
+        name: 'bycell',
+        isUnsupported: true,
+        type: h.prefix,
+        rule: h.prefixOp(/BYCELL/i),
+        map: (op: string, selection: Expression) => [op, selection],
+    },
+    {
+        '@desc': 'All rings of size ≤ 7 which have at least one atom in s1.',
+        '@examples': ['BYRING resn HEM'],
+        name: 'byring',
+        // isUnsupported: true, // structure-query.atom-set.atom-count' is not implemented.
+        type: h.prefix,
+        rule: h.prefixOp(/BYRING/i),
+        map: (op: string, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.intersectBy({
+                    '0': B.struct.filter.pick({
+                        '0': B.struct.generator.rings(),
+                        test: B.core.logic.and([
+                            B.core.rel.lte([B.struct.atomSet.atomCount(), 7]),
+                            B.core.rel.gr([B.struct.atomSet.countQuery([selection]), 1]),
+                        ]),
+                    }),
+                    by: selection,
+                })
+            );
+        },
+    },
+    {
+        '@desc': 'Selects atoms directly bonded to s1, excludes s1.',
+        '@examples': ['NEIGHBOR resn CYS'],
+        name: 'neighbor',
+        type: h.prefix,
+        abbr: ['nbr.'],
+        rule: h.prefixOp(/NEIGHBOR|nbr\./i),
+        map: (op: string, selection: Expression) => {
+            return B.struct.modifier.exceptBy({
+                '0': h.asAtoms(
+                    B.struct.modifier.includeConnected({
+                        '0': B.struct.modifier.union({ 0: selection }),
+                        'bond-test': true,
+                    })
+                ),
+                by: selection,
+            });
+        },
+    },
+    {
+        '@desc': 'Selects atoms directly bonded to s1, may include s1.',
+        '@examples': ['BOUND_TO name CA'],
+        name: 'bound_to',
+        abbr: ['bto.'],
+        type: h.prefix,
+        rule: h.prefixOp(/BOUND_TO|bto\./i),
+        map: (op: string, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.includeConnected({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                })
+            );
+        },
+    },
+    {
+        '@desc': 'Extends s1 by X bonds connected to atoms in s1.',
+        '@examples': ['resname LIG EXTEND 3'],
+        name: 'extend',
+        abbr: ['xt.'],
+        type: h.postfix,
+        rule: h.postfixOp(/(EXTEND|xt\.)\s+([0-9]+)/i, 2).map((x: any) => parseInt(x)),
+        map: (count: number, selection: Expression) => {
+            return h.asAtoms(
+                B.struct.modifier.includeConnected({
+                    '0': B.struct.modifier.union({ 0: selection }),
+                    'bond-test': true,
+                    'layer-count': count,
+                })
+            );
+        },
+    },
+];
diff --git a/src/mol-script/transpilers/pymol/parser.ts b/src/mol-script/transpilers/pymol/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b730b2a3e5ed019a4f5ac1e074f397a5f53a6480
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/parser.ts
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+// https://pymol.org/dokuwiki/doku.php?id=selection:alpha
+// https://pymolwiki.org/index.php/Selection_Algebra
+// https://github.com/evonove/pymol/blob/master/pymol/layer3/Selector.cpp
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+import { AtomGroupArgs } from '../types';
+import { Transpiler } from '../transpiler';
+
+const propertiesDict = h.getPropertyRules(properties);
+
+const slash = P.MonadicParser.string('/');
+
+function orNull(rule: P.MonadicParser<any>) {
+    return rule.or(P.MonadicParser.of(null));
+}
+
+function atomSelectionQuery(x: any) {
+    const tests: AtomGroupArgs = {};
+    const props: { [k: string]: any[] } = {};
+
+    for (const k in x) {
+        const ps = properties[k];
+        if (!ps) {
+            throw new Error(`property '${k}' not supported, value '${x[k]}'`);
+        }
+        if (x[k] === null) continue;
+        if (!props[ps.level]) props[ps.level] = [];
+        props[ps.level].push(x[k]);
+    }
+
+    for (const p in props) {
+        tests[p] = h.andExpr(props[p]);
+    }
+
+    return B.struct.generator.atomGroups(tests);
+}
+
+const lang = P.MonadicParser.createLanguage({
+    Parens: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Parens,
+            r.Operator,
+            r.Expression
+        ).wrap(P.MonadicParser.string('('), P.MonadicParser.string(')'));
+    },
+
+    Expression: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Keywords,
+            r.AtomSelectionMacro.map(atomSelectionQuery),
+            r.NamedAtomProperties,
+            r.Pepseq,
+            r.Rep,
+            r.Object
+        );
+    },
+
+    AtomSelectionMacro: function (r: any) {
+        return P.MonadicParser.alt(
+            slash.then(P.MonadicParser.alt(
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty).skip(slash),
+                    orNull(propertiesDict.segi).skip(slash),
+                    orNull(propertiesDict.chain).skip(slash),
+                    orNull(propertiesDict.resi).skip(slash),
+                    orNull(propertiesDict.name)
+                ).map(x => { return { object: x[0], segi: x[1], chain: x[2], resi: x[3], name: x[4] }; }),
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty).skip(slash),
+                    orNull(propertiesDict.segi).skip(slash),
+                    orNull(propertiesDict.chain).skip(slash),
+                    orNull(propertiesDict.resi)
+                ).map(x => { return { object: x[0], segi: x[1], chain: x[2], resi: x[3] }; }),
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty).skip(slash),
+                    orNull(propertiesDict.segi).skip(slash),
+                    orNull(propertiesDict.chain)
+                ).map(x => { return { object: x[0], segi: x[1], chain: x[2] }; }),
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty).skip(slash),
+                    orNull(propertiesDict.segi)
+                ).map(x => { return { object: x[0], segi: x[1] }; }),
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty)
+                ).map(x => { return { object: x[0] }; }),
+            )),
+            P.MonadicParser.alt(
+                P.MonadicParser.seq(
+                    orNull(r.ObjectProperty).skip(slash),
+                    orNull(propertiesDict.segi).skip(slash),
+                    orNull(propertiesDict.chain).skip(slash),
+                    orNull(propertiesDict.resi).skip(slash),
+                    orNull(propertiesDict.name)
+                ).map(x => { return { object: x[0], segi: x[1], chain: x[2], resi: x[3], name: x[4] }; }),
+                P.MonadicParser.seq(
+                    orNull(propertiesDict.segi).skip(slash),
+                    orNull(propertiesDict.chain).skip(slash),
+                    orNull(propertiesDict.resi).skip(slash),
+                    orNull(propertiesDict.name)
+                ).map(x => { return { segi: x[0], chain: x[1], resi: x[2], name: x[3] }; }),
+                P.MonadicParser.seq(
+                    orNull(propertiesDict.chain).skip(slash),
+                    orNull(propertiesDict.resi).skip(slash),
+                    orNull(propertiesDict.name)
+                ).map(x => { return { chain: x[0], resi: x[1], name: x[2] }; }),
+                P.MonadicParser.seq(
+                    orNull(propertiesDict.resi).skip(slash),
+                    orNull(propertiesDict.name)
+                ).map(x => { return { resi: x[0], name: x[1] }; }),
+            )
+        );
+    },
+
+    NamedAtomProperties: function () {
+        return P.MonadicParser.alt(...h.getNamedPropertyRules(properties));
+    },
+
+    Keywords: () => P.MonadicParser.alt(...h.getKeywordRules(keywords)),
+
+    ObjectProperty: () => {
+        const w = h.getReservedWords(properties, keywords, operators)
+            .sort(h.strLenSortFn).map(h.escapeRegExp).join('|');
+        return P.MonadicParser.regexp(new RegExp(`(?!(${w}))[A-Z0-9_]+`, 'i'));
+    },
+    Object: (r: any) => {
+        return r.ObjectProperty.notFollowedBy(slash)
+            .map((x: any) => { throw new Error(`property 'object' not supported, value '${x}'`); });
+    },
+
+    // Selects peptide sequence matching upper-case one-letter
+    // sequence SEQ (see also FindSeq).
+    // PEPSEQ seq
+    Pepseq: () => {
+        return P.MonadicParser.regexp(/(PEPSEQ|ps\.)\s+([a-z]+)/i, 2)
+            .map(h.makeError(`operator 'pepseq' not supported`));
+    },
+
+    // Selects atoms which show representation rep.
+    // REP rep
+    Rep: () => {
+        return P.MonadicParser.regexp(/REP\s+(lines|spheres|mesh|ribbon|cartoon|sticks|dots|surface|labels|extent|nonbonded|nb_spheres|slice|extent|slice|dashes|angles|dihedrals|cgo|cell|callback|everything)/i, 1)
+            .map(h.makeError(`operator 'rep' not supported`));
+    },
+
+    Operator: function (r: any) {
+        return h.combineOperators(operators, P.MonadicParser.alt(r.Parens, r.Expression, r.Operator));
+    },
+
+    Query: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Operator,
+            r.Parens,
+            r.Expression
+        ).trim(P.MonadicParser.optWhitespace);
+    }
+});
+
+export const transpiler: Transpiler = str => lang.Query.tryParse(str);
diff --git a/src/mol-script/transpilers/pymol/properties.ts b/src/mol-script/transpilers/pymol/properties.ts
new file mode 100644
index 0000000000000000000000000000000000000000..04c4bb716d5168507a81f1e211e879552c041b31
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/properties.ts
@@ -0,0 +1,215 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { PropertyDict } from '../types';
+
+const reFloat = /[-+]?[0-9]*\.?[0-9]+/;
+
+function atomNameListMap(x: string) { return x.split('+').map(B.atomName); }
+function listMap(x: string) { return x.split('+').map(x => x.replace(/^["']|["']$/g, '')); }
+
+function listOrRangeMap(x: string) {
+    // cases
+    if (x.includes('-') && x.includes('+')) {
+        const pSplit = x.split('+').map(x => x.replace(/^["']|["']$/g, ''));
+        const res: number[] = [];
+        pSplit.forEach(x => {
+            if (x.includes('-') && !x.startsWith('-')) {
+                const [min, max] = x.split('-').map(x => parseInt(x));
+                for (let i = min; i <= max; i++) {
+                    res.push(i);
+                }
+            } else if (x.includes('-') && x.startsWith('-') && x.match(/[0-9]+-[-0-9]+/)) {
+                const min = -parseInt(x.split('-')[1]);
+                let max;
+                if (x.includes('--')) {
+                    max = -parseInt(x.split('-')[3]);
+                } else {
+                    max = parseInt(x.split('-')[2]);
+                }
+                for (let i = min; i <= max; i++) {
+                    res.push(i);
+                }
+            } else if (x.includes('-') && x.startsWith('-') && !x.match(/[0-9]+-[-0-9]+/)) {
+                res.push(parseInt(x));
+            } else {
+                res.push(parseInt(x));
+            }
+        });
+        return res;
+    } else if (x.includes('-') && !x.includes('+')) {
+        const res: number[] = [];
+        if (!x.startsWith('-')) {
+            const [min, max] = x.split('-').map(x => parseInt(x));
+            for (let i = min; i <= max; i++) {
+                res.push(i);
+            }
+        } else if (x.startsWith('-') && x.match(/[0-9]+-[-0-9]+/)) {
+            const min = -parseInt(x.split('-')[1]);
+            let max;
+            if (x.includes('--')) {
+                max = -parseInt(x.split('-')[3]);
+            } else {
+                max = parseInt(x.split('-')[2]);
+            }
+            for (let i = min; i <= max; i++) {
+                res.push(i);
+            }
+        } else if (x.startsWith('-') && !x.match(/[0-9]+-[-0-9]+/)) {
+            res.push(parseInt(x));
+        } else {
+            res.push(parseInt(x));
+        }
+        return res;
+    } else if (!x.includes('-') && x.includes('+')) {
+        return listMap(x).map(x => parseInt(x));
+    } else {
+        return [parseInt(x)];
+    }
+}
+
+function elementListMap(x: string) {
+    return x.split('+').map(B.struct.type.elementSymbol);
+}
+
+const sstrucDict: { [k: string]: string } = {
+    H: 'helix',
+    S: 'beta',
+    L: 'none'
+};
+function sstrucListMap(x: string) {
+    return {
+        flags: B.struct.type.secondaryStructureFlags(
+            x.toUpperCase().split('+').map(ss => sstrucDict[ss] || 'none')
+        )
+    };
+}
+
+export const properties: PropertyDict = {
+    symbol: {
+        '@desc': 'chemical-symbol-list: list of 1- or 2-letter chemical symbols from the periodic table',
+        '@examples': ['symbol O+N'],
+        abbr: ['e.'], regex: /[a-zA-Z'"+]+/, map: elementListMap,
+        level: 'atom-test', property: B.acp('elementSymbol')
+    },
+    name: {
+        '@desc': 'atom-name-list: list of up to 4-letter codes for atoms in proteins or nucleic acids',
+        '@examples': ['name CA+CB+CG+CD'],
+        abbr: ['n.'], regex: /[a-zA-Z0-9'"+]+/, map: atomNameListMap,
+        level: 'atom-test', property: B.ammp('label_atom_id')
+    },
+    resn: {
+        '@desc': 'residue-name-list: list of 3-letter codes for amino acids or list of up to 2-letter codes for nucleic acids',
+        '@examples': ['resn ASP+GLU+ASN+GLN', 'resn A+G'],
+        abbr: ['resname', 'r.'], regex: /[a-zA-Z0-9'"+]+/, map: listMap,
+        level: 'residue-test', property: B.ammp('label_comp_id')
+    },
+    resi: {
+        '@desc': 'residue-identifier-list list of up to 4-digit residue numbers or residue-identifier-range',
+        '@examples': ['resi 1+10+100+1000', 'resi 1-10'],
+        abbr: ['resident', 'residue', 'resid', 'i.'], regex: /[0-9+-]+/, map: listOrRangeMap,
+        level: 'residue-test', property: B.ammp('auth_seq_id')
+    },
+    alt: {
+        '@desc': 'alternate-conformation-identifier-list list of single letters',
+        '@examples': ['alt A+B', 'alt ""', 'alt ""+A'],
+        abbr: [], regex: /[a-zA-Z0-9'"+]+/, map: listMap,
+        level: 'atom-test', property: B.ammp('label_alt_id')
+    },
+    chain: {
+        '@desc': 'chain-identifier-list list of single letters or sometimes numbers',
+        '@examples': ['chain A'],
+        abbr: ['c.'], regex: /[a-zA-Z0-9'"+]+/, map: listMap,
+        level: 'chain-test', property: B.ammp('auth_asym_id')
+    },
+    segi: {
+        '@desc': 'segment-identifier-list list of up to 4 letter identifiers',
+        '@examples': ['segi lig'],
+        abbr: ['segid', 's.'], regex: /[a-zA-Z0-9'"+]+/, map: listMap,
+        level: 'chain-test', property: B.ammp('label_asym_id')
+    },
+    flag: {
+        '@desc': 'flag-number a single integer from 0 to 31',
+        '@examples': ['flag 0'],
+        isUnsupported: true,
+        abbr: ['f.'], regex: /[0-9]+/, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    numeric_type: {
+        '@desc': 'type-number a single integer',
+        '@examples': ['nt. 5'],
+        isUnsupported: true,
+        abbr: ['nt.'], regex: /[0-9]+/, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    text_type: {
+        '@desc': 'type-string a list of up to 4 letter codes',
+        '@examples': ['text_type HA+HC'],
+        isUnsupported: true,
+        abbr: ['tt.'], regex: /[a-zA-Z0-9'"+]+/, map: listMap,
+        level: 'atom-test'
+    },
+    id: {
+        '@desc': 'external-index-number a single integer',
+        '@examples': ['id 23'],
+        regex: /[0-9+-]+/, map: listOrRangeMap,
+        level: 'atom-test', property: B.ammp('id')
+    },
+    index: {
+        '@desc': 'internal-index-number a single integer',
+        '@examples': ['index 11'],
+        regex: /[0-9+-]+/, map: listOrRangeMap,
+        level: 'atom-test', property: B.ammp('id')
+    },
+    ss: {
+        '@desc': 'secondary-structure-type list of single letters. Helical regions should be assigned H and sheet regions S. Loop regions can either be assigned L or be blank.',
+        '@examples': ['ss H+S+L', 'ss S+""'],
+        abbr: [], regex: /[a-zA-Z'"+]+/, map: sstrucListMap,
+        level: 'residue-test', property: B.ammp('secondaryStructureFlags')
+    },
+
+    b: {
+        '@desc': 'comparison-operator b-factor-value a real number',
+        '@examples': ['b > 10'],
+        isNumeric: true,
+        abbr: [], regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('B_iso_or_equiv')
+    },
+    q: {
+        '@desc': 'comparison-operator occupancy-value a real number',
+        '@examples': ['q <0.50'],
+        isNumeric: true,
+        abbr: [], regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('occupancy')
+    },
+    formal_charge: {
+        '@desc': 'comparison-operator formal charge-value an integer',
+        '@examples': ['fc. = -1'],
+        isNumeric: true,
+        abbr: ['fc.'], regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('pdbx_formal_charge')
+    },
+    partial_charge: {
+        '@desc': 'comparison-operator partial charge-value a real number',
+        '@examples': ['pc. > 1'],
+        isUnsupported: true,
+        isNumeric: true,
+        abbr: ['pc.'], regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    elem: {
+        '@desc': 'str  atomic element symbol string ("X" if undefined)',
+        '@examples': ['elem N'],
+        regex: /[a-zA-Z0-9]{1,3}/, map: x => B.es(x),
+        level: 'atom-test', property: B.acp('elementSymbol')
+    }
+};
diff --git a/src/mol-script/transpilers/pymol/symbols.ts b/src/mol-script/transpilers/pymol/symbols.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e1c89586e408cea1880a9d38766c0eaa100c646
--- /dev/null
+++ b/src/mol-script/transpilers/pymol/symbols.ts
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+
+export const Properties: string[] = [];
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+    Properties.push(name);
+    if (properties[name].abbr) Properties.push(...properties[name].abbr!);
+}
+
+export const Operators: string[] = [];
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+    Operators.push(o.name);
+    if (o.abbr) Operators.push(...o.abbr);
+});
+
+export const Keywords: string[] = [];
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+    Keywords.push(name);
+    if (keywords[name].abbr) Keywords.push(...keywords[name].abbr!);
+}
+
+export const all = { Properties, Operators: [...Operators, 'of'], Keywords };
diff --git a/src/mol-script/transpilers/transpiler.ts b/src/mol-script/transpilers/transpiler.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1fe04bfc150a23330ec19e57870e5211f266c3ad
--- /dev/null
+++ b/src/mol-script/transpilers/transpiler.ts
@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { Expression } from '../language/expression';
+
+export type Transpiler = (source: string) => Expression
+
+export const Transpiler = (source: string) => Expression;
diff --git a/src/mol-script/transpilers/types.ts b/src/mol-script/transpilers/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1589b804262f896fe5bb817df51c85ba3df4a8c1
--- /dev/null
+++ b/src/mol-script/transpilers/types.ts
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../mol-util/monadic-parser';
+import { Expression } from '../language/expression';
+
+export interface AtomGroupArgs {
+    [index: string]: any
+    'entity-test'?: Expression
+    'chain-test'?: Expression
+    'residue-test'?: Expression
+    'atom-test'?: Expression
+    'groupBy'?: Expression
+}
+
+export interface Keyword {
+    '@desc': string
+    abbr?: string[]
+    map?: () => Expression /* not given means the keyword is unsupported */
+}
+
+export type KeywordDict = { [name: string]: Keyword }
+
+export interface Property {
+    '@desc': string
+    '@examples': string[]
+    isUnsupported?: boolean
+    isNumeric?: boolean
+    abbr?: string[]
+    regex: RegExp
+    map: (s: string) => any
+    level: 'atom-test' | 'residue-test' | 'chain-test' | 'entity-test'
+    property?: Expression
+}
+
+export type PropertyDict = { [name: string]: Property }
+
+export interface Operator {
+    '@desc': string
+    '@examples': string[]
+    name: string
+    abbr?: string[]
+    isUnsupported?: boolean
+    type: (p1: P.MonadicParser<any>, p2: P.MonadicParser<any>, fn: any) => P.MonadicParser<any>
+    rule: P.MonadicParser<any>
+    map: (x: any, y: any, z?: any) => Expression | Expression[]
+}
+
+export type OperatorList = Operator[]
+
+export interface Function {
+    '@desc': string
+    '@examples': string[]
+    map?: (x: any) => Expression /* not given means the keyword is unsupported */
+}
+
+export type FunctionDict = { [name: string]: Function }
diff --git a/src/mol-script/transpilers/vmd/examples.ts b/src/mol-script/transpilers/vmd/examples.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fbc65d3bb12e29f4ad5c3d2fad2365f4757d52b4
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/examples.ts
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+export const examples = [{
+    name: 'All water residues',
+    value: 'water'
+}, {
+    name: 'All C-alpha atoms',
+    value: 'name CA'
+}, {
+    name: 'Residue 35',
+    value: 'resid 35'
+}, {
+    name: 'C-alpha atoms of ALA',
+    value: 'name CA and resname ALA'
+}, {
+    name: 'Backbone atoms',
+    value: 'backbone'
+}, {
+    name: 'Non-protein atoms',
+    value: 'not protein'
+}, {
+    name: 'Protein backbone or hydrogen atoms',
+    value: 'protein (backbone or name H)'
+}, {
+    name: 'Atoms heavier than 20',
+    value: 'mass > 20'
+}, {
+    name: 'Atoms with two bonds',
+    value: 'numbonds = 2'
+}, {
+    name: 'Atoms with an absolute charge greater 1',
+    value: 'abs(charge) > 1'
+}, {
+    name: 'Atoms with an x coordinate between -25 and -20',
+    value: 'x < -20 and x > -25'
+}, {
+    name: 'Helices',
+    value: 'structure H'
+}, {
+    name: 'Atoms with name "A 1"',
+    value: "name 'A 1'"
+}, {
+    name: 'Atoms with name "A *"',
+    value: "name 'A *'"
+}, {
+    name: 'Atoms with names starting with C',
+    value: 'name "C.*"'
+}, {
+    name: 'Atoms within 10 ang of [25, 15, 10]',
+    value: 'sqr(x+25)+sqr(y+15)+sqr(z+10) <= sqr(10)'
+}, {
+    name: 'Atoms within 5 ang of iron atoms',
+    value: 'within 5 of name FE'
+}, {
+    name: 'Atoms around 10 ang of HEM residue',
+    value: 'exwithin 10 of resname HEM'
+}, {
+    name: 'ALA residues within 15 ang of HEM',
+    value: 'resname ALA within 15 of resname HEM'
+}, {
+    name: 'All groups that include an iron atom',
+    value: 'same resid as name FE'
+}, {
+    name: 'Atoms with mass between 12 and 17.5',
+    value: 'mass 12 to 17.5'
+}, {
+    name: 'Residues 60, 80, 90 and 142',
+    value: 'resid 60 80 90 142'
+}/* , {
+    name: 'Residues ala, arg, asn, asp, cys, and tyr',
+    value: 'resname ALA to CYS TYR'
+}*/];
diff --git a/src/mol-script/transpilers/vmd/functions.ts b/src/mol-script/transpilers/vmd/functions.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a2db414a922cf3001456cd8c180ac38474ccbe2e
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/functions.ts
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2017-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { FunctionDict } from '../types';
+
+export const functions: FunctionDict = {
+    'sqr': {
+        '@desc': 'square of x',
+        '@examples': ['sqr(2)'],
+        map: x => B.core.math.pow([x, 2]),
+    },
+    'sqrt': {
+        '@desc': 'square root of x',
+        '@examples': ['sqrt(2)'],
+        map: x => B.core.math.sqrt([x]),
+    },
+    'abs': {
+        '@desc': 'absolute value of x',
+        '@examples': ['abs(2)'],
+        map: x => B.core.math.abs([x]),
+    },
+    'floor': {
+        '@desc': 'largest integer not greater than x',
+        '@examples': ['floor(2)'],
+        map: x => B.core.math.floor([x]),
+    },
+    'ceil': {
+        '@desc': 'smallest integer not less than x',
+        '@examples': ['ceil(2)'],
+        map: x => B.core.math.ceil([x]),
+    },
+    'sin': {
+        '@desc': 'sine of x',
+        '@examples': ['sin(2)'],
+        map: x => B.core.math.sin([x]),
+    },
+    'cos': {
+        '@desc': 'cosine of x',
+        '@examples': ['cos(2)'],
+        map: x => B.core.math.cos([x]),
+    },
+    'tan': {
+        '@desc': 'tangent of x',
+        '@examples': ['tan(2)'],
+        map: x => B.core.math.tan([x]),
+    },
+    'atan': {
+        '@desc': 'arctangent of x',
+        '@examples': ['atan(2)'],
+        map: x => B.core.math.atan([x]),
+    },
+    'asin': {
+        '@desc': 'arcsin of x',
+        '@examples': ['asin(2)'],
+        map: x => B.core.math.asin([x]),
+    },
+    'acos': {
+        '@desc': 'arccos of x',
+        '@examples': ['acos(2)'],
+        map: x => B.core.math.acos([x]),
+    },
+    'sinh': {
+        '@desc': 'hyperbolic sine of x',
+        '@examples': ['sinh(2)'],
+        map: x => B.core.math.sinh([x]),
+    },
+    'cosh': {
+        '@desc': 'hyperbolic cosine of x',
+        '@examples': ['cosh(2)'],
+        map: x => B.core.math.cosh([x]),
+    },
+    'tanh': {
+        '@desc': 'hyperbolic tangent of x',
+        '@examples': ['tanh(2)'],
+        map: x => B.core.math.tanh([x]),
+    },
+    'exp': {
+        '@desc': 'e to the power x',
+        '@examples': ['exp(2)'],
+        map: x => B.core.math.exp([x]),
+    },
+    'log': {
+        '@desc': 'natural log of x',
+        '@examples': ['log(2)'],
+        map: x => B.core.math.log([x]),
+    },
+    'log10': {
+        '@desc': 'log base 10 of x',
+        '@examples': ['log10(2)'],
+        map: x => B.core.math.log10([x]),
+    }
+};
diff --git a/src/mol-script/transpilers/vmd/keywords.ts b/src/mol-script/transpilers/vmd/keywords.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8d783bf627806f13bd27f42f9fca81043c578cfa
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/keywords.ts
@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { KeywordDict } from '../types';
+
+function proteinExpr() {
+    return B.struct.filter.pick({
+        0: B.struct.generator.atomGroups({
+            'group-by': B.ammp('residueKey')
+        }),
+        test: B.core.set.isSubset([
+            h.atomNameSet(['C', 'N', 'CA', 'O']),
+            B.ammpSet('label_atom_id')
+        ])
+    });
+}
+
+function nucleicExpr() {
+    return B.struct.filter.pick({
+        0: B.struct.generator.atomGroups({
+            'group-by': B.ammp('residueKey')
+        }),
+        test: B.core.logic.and([
+            B.core.set.isSubset([
+                h.atomNameSet(['P']),
+                B.ammpSet('label_atom_id')
+            ]),
+            B.core.logic.or([
+                B.core.set.isSubset([
+                    h.atomNameSet(["O3'", "C3'", "C4'", "C5'", "O5'"]),
+                    B.ammpSet('label_atom_id')
+                ]),
+                B.core.set.isSubset([
+                    h.atomNameSet(['O3*', 'C3*', 'C4*', 'C5*', 'O5*']),
+                    B.ammpSet('label_atom_id')
+                ])
+            ])
+        ])
+    });
+}
+
+function backboneExpr() {
+    return B.struct.combinator.merge([
+        B.struct.generator.queryInSelection({
+            0: proteinExpr(),
+            query: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    h.atomNameSet(Backbone.protein),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        }),
+        B.struct.generator.queryInSelection({
+            0: nucleicExpr(),
+            query: B.struct.generator.atomGroups({
+                'atom-test': B.core.set.has([
+                    h.atomNameSet(Backbone.nucleic),
+                    B.ammp('label_atom_id')
+                ])
+            })
+        })
+    ]);
+}
+
+function secStrucExpr(flags: string[]) {
+    return B.struct.generator.atomGroups({
+        'residue-test': B.core.flags.hasAll([
+            B.ammp('secondaryStructureFlags'),
+            B.struct.type.secondaryStructureFlags(flags)
+        ])
+    });
+}
+
+const Backbone = {
+    nucleic: ['P', "O3'", "O5'", "C5'", "C4'", "C3'", 'OP1', 'OP2', 'O3*', 'O5*', 'C5*', 'C4*', 'C3*'],
+    protein: ['C', 'N', 'CA', 'O']
+};
+
+const ResDict = {
+    acidic: ['ASP', 'GLU'],
+    aliphatic: ['ALA', 'GLY', 'ILE', 'LEU', 'VAL'],
+    aromatic: ['HIS', 'PHE', 'TRP', 'TYR'],
+    at: ['ADA', 'A', 'THY', 'T'],
+    basic: ['ARG', 'HIS', 'LYS'],
+    buried: ['ALA', 'LEU', 'VAL', 'ILE', 'PHE', 'CYS', 'MET', 'TRP'],
+    cg: ['CYT', 'C', 'GUA', 'G'],
+    cyclic: ['HIS', 'PHE', 'PRO', 'TRP', 'TYR'],
+    hydrophobic: ['ALA', 'LEU', 'VAL', 'ILE', 'PRO', 'PHE', 'MET', 'TRP'],
+    medium: ['VAL', 'THR', 'ASP', 'ASN', 'PRO', 'CYS', 'ASX', 'PCA', 'HYP'],
+    neutral: ['VAL', 'PHE', 'GLN', 'TYR', 'HIS', 'CYS', 'MET', 'TRP', 'ASX', 'GLX', 'PCA', 'HYP'],
+    purine: ['ADE', 'A', 'GUA', 'G'],
+    pyrimidine: ['CYT', 'C', 'THY', 'T', 'URI', 'U'],
+    small: ['ALA', 'GLY', 'SER'],
+    water: ['H2O', 'HH0', 'OHH', 'HOH', 'OH2', 'SOL', 'WAT', 'TIP', 'TIP2', 'TIP3', 'TIP4']
+};
+
+export const keywords: KeywordDict = {
+    all: {
+        '@desc': 'everything',
+        map: () => B.struct.generator.all()
+    },
+    none: {
+        '@desc': 'nothing',
+        map: () => B.struct.generator.empty()
+    },
+    protein: {
+        '@desc': 'a residue with atoms named C, N, CA, and O',
+        map: () => proteinExpr()
+    },
+    nucleic: {
+        '@desc': "a residue with atoms named P, O1P, O2P and either O3', C3', C4', C5', O5' or O3*, C3*, C4*, C5*, O5*. This definition assumes that the base is phosphorylated, an assumption which will be corrected in the future.",
+        map: () => nucleicExpr()
+    },
+    backbone: {
+        '@desc': 'the C, N, CA, and O atoms of a protein and the equivalent atoms in a nucleic acid.',
+        map: () => backboneExpr()
+    },
+    sidechain: {
+        '@desc': 'non-backbone atoms and bonds', // TODO: what does 'bonds' mean here?
+        map: () => h.invertExpr(backboneExpr())
+    },
+    water: {
+        '@desc': 'all atoms with the resname H2O, HH0, OHH, HOH, OH2, SOL, WAT, TIP, TIP2, TIP3 or TIP4',
+        abbr: ['waters'],
+        map: () => h.resnameExpr(ResDict.water)
+    },
+    at: {
+        '@desc': 'residues named ADA A THY T',
+        map: () => h.resnameExpr(ResDict.at)
+    },
+    acidic: {
+        '@desc': 'residues named ASP GLU',
+        map: () => h.resnameExpr(ResDict.acidic)
+    },
+    acyclic: {
+        '@desc': '"protein and not cyclic"',
+        map: () => B.struct.modifier.intersectBy({
+            0: proteinExpr(),
+            by: h.invertExpr(h.resnameExpr(ResDict.cyclic))
+        })
+    },
+    aliphatic: {
+        '@desc': 'residues named ALA GLY ILE LEU VAL',
+        map: () => h.resnameExpr(ResDict.aliphatic)
+    },
+    alpha: {
+        '@desc': "atom's residue is an alpha helix",
+        map: () => secStrucExpr(['alpha'])
+    },
+    amino: {
+        '@desc': 'a residue with atoms named C, N, CA, and O',
+        map: () => proteinExpr()
+    },
+    aromatic: {
+        '@desc': 'residues named HIS PHE TRP TYR',
+        map: () => h.resnameExpr(ResDict.aromatic)
+    },
+    basic: {
+        '@desc': 'residues named ARG HIS LYS',
+        map: () => h.resnameExpr(ResDict.basic)
+    },
+    bonded: {
+        '@desc': 'atoms for which numbonds > 0',
+        map: () => h.asAtoms(B.struct.filter.pick({
+            '0': B.struct.modifier.includeConnected({
+                '0': B.struct.generator.all(),
+                'bond-test': B.core.flags.hasAny([
+                    B.struct.bondProperty.flags(),
+                    B.struct.type.bondFlags(['covalent', 'metallic', 'sulfide'])
+                ])
+            }),
+            test: B.core.rel.gr([
+                B.struct.atomSet.atomCount(), 1
+            ])
+        }))
+    },
+    buried: {
+        '@desc': 'residues named ALA LEU VAL ILE PHE CYS MET TRP',
+        map: () => h.resnameExpr(ResDict.buried)
+    },
+    cg: {
+        '@desc': 'residues named CYT C GUA G',
+        map: () => h.resnameExpr(ResDict.cg)
+    },
+    charged: {
+        '@desc': '"basic or acidic"',
+        map: () => h.resnameExpr(ResDict.basic.concat(ResDict.acidic))
+    },
+    cyclic: {
+        '@desc': 'residues named HIS PHE PRO TRP TYR',
+        map: () => h.resnameExpr(ResDict.cyclic)
+    },
+    hetero: {
+        '@desc': '"not (protein or nucleic)"',
+        map: () => h.invertExpr(
+            B.struct.combinator.merge([proteinExpr(), nucleicExpr()])
+        )
+    },
+    hydrogen: {
+        '@desc': 'name "[0-9]?H.*"',
+        map: () => B.struct.generator.atomGroups({
+            'atom-test': B.core.str.match([
+                B.core.type.regex(['^[0-9]?[H].*$', 'i']),
+                B.core.type.str([B.ammp('label_atom_id')])
+            ])
+        })
+    },
+    large: {
+        '@desc': '"protein and not (small or medium)"',
+        map: () => B.struct.modifier.intersectBy({
+            0: proteinExpr(),
+            by: h.invertExpr(
+                h.resnameExpr(ResDict.small.concat(ResDict.medium))
+            )
+        })
+    },
+    medium: {
+        '@desc': 'residues named VAL THR ASP ASN PRO CYS ASX PCA HYP',
+        map: () => h.resnameExpr(ResDict.medium)
+    },
+    neutral: {
+        '@desc': 'residues named VAL PHE GLN TYR HIS CYS MET TRP ASX GLX PCA HYP',
+        map: () => h.resnameExpr(ResDict.neutral)
+    },
+    hydrophobic: {
+        '@desc': 'hydrophobic resname ALA LEU VAL ILE PRO PHE MET TRP',
+        map: () => h.resnameExpr(ResDict.hydrophobic)
+    },
+    polar: {
+        '@desc': '"protein and not hydrophobic"',
+        map: () => B.struct.modifier.intersectBy({
+            0: proteinExpr(),
+            by: h.invertExpr(h.resnameExpr(ResDict.hydrophobic))
+        })
+    },
+    purine: {
+        '@desc': 'residues named ADE A GUA G',
+        map: () => h.resnameExpr(ResDict.purine)
+    },
+    pyrimidine: {
+        '@desc': 'residues named CYT C THY T URI U',
+        map: () => h.resnameExpr(ResDict.pyrimidine)
+    },
+    small: {
+        '@desc': 'residues named ALA GLY SER',
+        map: () => h.resnameExpr(ResDict.small)
+    },
+    surface: {
+        '@desc': '"protein and not buried"',
+        map: () => B.struct.modifier.intersectBy({
+            0: proteinExpr(),
+            by: h.invertExpr(h.resnameExpr(ResDict.buried))
+        })
+    },
+    alpha_helix: {
+        '@desc': "atom's residue is in an alpha helix",
+        map: () => secStrucExpr(['alpha'])
+    },
+    pi_helix: {
+        '@desc': "atom's residue is in a pi helix",
+        map: () => secStrucExpr(['pi'])
+    },
+    helix_3_10: {
+        '@desc': "atom's residue is in a 3-10 helix",
+        map: () => secStrucExpr(['3-10'])
+    },
+    helix: {
+        '@desc': "atom's residue is in an alpha or pi or 3-10 helix",
+        map: () => secStrucExpr(['helix'])
+    },
+    extended_beta: {
+        '@desc': "atom's residue is a beta sheet",
+        map: () => secStrucExpr(['sheet'])
+    },
+    bridge_beta: {
+        '@desc': "atom's residue is a beta sheet",
+        map: () => secStrucExpr(['strand'])
+    },
+    sheet: {
+        '@desc': "atom's residue is a beta sheet",
+        map: () => secStrucExpr(['beta'])
+    },
+    turn: {
+        '@desc': "atom's residue is in a turn conformation",
+        map: () => secStrucExpr(['turn'])
+    },
+    coil: {
+        '@desc': "atom's residue is in a coil conformation",
+        map: () => B.struct.modifier.intersectBy({
+            0: proteinExpr(),
+            by: secStrucExpr(['none'])
+        })
+    }
+};
diff --git a/src/mol-script/transpilers/vmd/markdown-docs.ts b/src/mol-script/transpilers/vmd/markdown-docs.ts
new file mode 100644
index 0000000000000000000000000000000000000000..63a44f9ce3dc120cf0205627380be29627f58d5a
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/markdown-docs.ts
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+import { functions } from './functions';
+
+const _docs: string[] = [
+    'VMD',
+    '============',
+    '--------------------------------',
+    ''
+];
+
+_docs.push(`## Properties\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+
+    const names = [name];
+    if (properties[name].abbr) names.push(...properties[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (properties[name]['@desc']) {
+        _docs.push(`*${properties[name]['@desc']}*\n`);
+    }
+}
+
+_docs.push(`## Operators\n\n`);
+_docs.push('--------------------------------\n');
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+
+    const names = [o.name];
+    if (o.abbr) names.push(...o.abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (o['@desc']) {
+        _docs.push(`*${o['@desc']}*\n`);
+    }
+});
+
+_docs.push(`## Keywords\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+
+    const names = [name];
+    if (keywords[name].abbr) names.push(...keywords[name].abbr!);
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (keywords[name]['@desc']) {
+        _docs.push(`*${keywords[name]['@desc']}*\n`);
+    }
+}
+
+_docs.push(`## Functions\n\n`);
+_docs.push('--------------------------------\n');
+for (const name in functions) {
+    if (!functions[name].map) continue;
+
+    const names = [name];
+    _docs.push(`\`\`\`\n${names.join(', ')}\n\`\`\`\n`);
+
+    if (functions[name]['@desc']) {
+        _docs.push(`*${functions[name]['@desc']}*\n`);
+    }
+}
+
+export const docs = _docs.join('\n');
\ No newline at end of file
diff --git a/src/mol-script/transpilers/vmd/operators.ts b/src/mol-script/transpilers/vmd/operators.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e3cddd364691a4aafa7489e91a7e2eac2a7e826e
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/operators.ts
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { properties } from './properties';
+import { Expression } from '../../language/expression';
+import { OperatorList } from '../types';
+
+const propNames = Object.keys(properties).sort(h.strLenSortFn)
+    .filter(name => !properties[name].isUnsupported).join('|');
+
+export const operators: OperatorList = [
+    {
+        '@desc': 'Selects atoms that are not included in s1.',
+        '@examples': ['not protein'],
+        name: 'not',
+        type: h.prefix,
+        rule: P.MonadicParser.regexp(/NOT/i).skip(P.MonadicParser.whitespace),
+        map: (op, selection) => h.invertExpr(selection),
+    },
+    {
+        '@desc': 'Selects atoms within a specified distance of a selection',
+        '@examples': ['within 5 of name FE'],
+        name: 'within',
+        type: h.prefix,
+        rule: h.prefixOp(/WITHIN\s+([-+]?[0-9]*\.?[0-9]+)\s+OF/i, 1).map((x: any) => parseFloat(x)),
+        map: (radius: number, selection: Expression) => {
+            return B.struct.modifier.includeSurroundings({ 0: selection, radius });
+        }
+    },
+    {
+        '@desc': 'Exclusive within, equivalent to (within 3 of X) and not X',
+        '@examples': ['exwithin 10 of resname HEM'],
+        name: 'exwithin',
+        type: h.prefix,
+        rule: h.prefixOp(/EXWITHIN\s+([-+]?[0-9]*\.?[0-9]+)\s+OF/i, 1).map((x: any) => parseFloat(x)),
+        map: (radius: number, target: Expression) => {
+            return B.struct.modifier.exceptBy({
+                '0': B.struct.modifier.includeSurroundings({ 0: target, radius }),
+                by: target
+            });
+        }
+    },
+    {
+        '@desc': 'Selects atoms which have the same keyword as the atoms in a given selection',
+        '@examples': ['same resid as name FE'],
+        name: 'same',
+        type: h.prefix,
+        rule: h.prefixOp(new RegExp(`SAME\\s+(${propNames})\\s+AS`, 'i'), 1).map((x: any) => properties[x].property),
+        map: (property: Expression, source: Expression) => {
+            return B.struct.filter.withSameAtomProperties({
+                '0': B.struct.generator.all(),
+                source,
+                property
+            });
+        }
+    },
+    {
+        '@desc': 'Selects atoms included in both s1 and s2.',
+        '@examples': ['backbone and protein'],
+        name: 'and',
+        type: h.binaryLeft,
+        rule: P.MonadicParser.alt(h.infixOp(/AND/i), P.MonadicParser.whitespace),
+        map: (op, selection, by) => B.struct.modifier.intersectBy({ 0: selection, by })
+    },
+    {
+        '@desc': 'Selects atoms included in either s1 or s2.',
+        '@examples': ['water or protein'],
+        name: 'or',
+        type: h.binaryLeft,
+        rule: h.infixOp(/OR/i),
+        map: (op, s1, s2) => B.struct.combinator.merge([s1, s2])
+    }
+];
diff --git a/src/mol-script/transpilers/vmd/parser.ts b/src/mol-script/transpilers/vmd/parser.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dbe9b7bda9f1386bc3c4e54a8ab33e023d4e4f5e
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/parser.ts
@@ -0,0 +1,262 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import * as P from '../../../mol-util/monadic-parser';
+import * as h from '../helper';
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { sstrucMap, sstrucDict, properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+import { functions } from './functions';
+import { OperatorList } from '../types';
+import { Transpiler } from '../transpiler';
+
+// <, <=, = or ==, >=, >, and !=
+// lt, le, eq, ge, gt, and ne, =~
+const valueOperators: OperatorList = [
+    {
+        '@desc': 'multiplication, division',
+        '@examples': [],
+        name: 'mul-div',
+        type: h.binaryLeft,
+        rule: P.MonadicParser.regexp(/\s*(\*|\/)\s*/, 1),
+        map: (op, e1, e2) => {
+            switch (op) {
+                case '*': return B.core.math.mult([e1, e2]);
+                case '/': return B.core.math.div([e1, e2]);
+                default: throw new Error(`value operator '${op}' not supported`);
+            }
+        }
+    },
+    {
+        '@desc': 'addition, substraction',
+        '@examples': [],
+        name: 'add-sub',
+        type: h.binaryLeft,
+        rule: P.MonadicParser.regexp(/\s*(-|\+)\s*/, 1),
+        map: (op, e1, e2) => {
+            switch (op) {
+                case '-': return B.core.math.sub([e1, e2]);
+                case '+': return B.core.math.add([e1, e2]);
+                default: throw new Error(`value operator '${op}' not supported`);
+            }
+        }
+    },
+    {
+        '@desc': 'value comparisons',
+        '@examples': [],
+        name: 'comparison',
+        type: h.binaryLeft,
+        rule: P.MonadicParser.alt(P.MonadicParser.regexp(/\s*(=~|==|>=|<=|=|!=|>|<)\s*/, 1), P.MonadicParser.whitespace.result('=')),
+        map: (op, e1, e2) => {
+            let expr;
+            if (e1.head !== undefined) {
+                if (e1.head.name === 'structure-query.atom-property.macromolecular.secondary-structure-flags') {
+                    expr = B.core.flags.hasAny([e1, sstrucMap(e2)]);
+                }
+                if (e1.head.name === 'core.type.regex') {
+                    expr = B.core.str.match([e1, B.core.type.str([e2])]);
+                }
+            } else if (e2.head !== undefined) {
+                if (e2.head.name === 'structure-query.atom-property.macromolecular.secondary-structure-flags') {
+                    expr = B.core.flags.hasAny([e2, sstrucMap(e1)]);
+                }
+                if (e2.head.name === 'core.type.regex') {
+                    expr = B.core.str.match([e2, B.core.type.str([e1])]);
+                }
+            } else if (op === '=~') {
+                if (e1.head) {
+                    expr = B.core.str.match([
+                        B.core.type.regex([`^${e2}$`, 'i']),
+                        B.core.type.str([e1])
+                    ]);
+                } else {
+                    expr = B.core.str.match([
+                        B.core.type.regex([`^${e1}$`, 'i']),
+                        B.core.type.str([e2])
+                    ]);
+                }
+            }
+            if (!expr) {
+                if (e1.head) e2 = h.wrapValue(e1, e2);
+                if (e2.head) e1 = h.wrapValue(e2, e1);
+                switch (op) {
+                    case '=':
+                    case '==':
+                        expr = B.core.rel.eq([e1, e2]);
+                        break;
+                    case '!=':
+                        expr = B.core.rel.neq([e1, e2]);
+                        break;
+                    case '>':
+                        expr = B.core.rel.gr([e1, e2]);
+                        break;
+                    case '<':
+                        expr = B.core.rel.lt([e1, e2]);
+                        break;
+                    case '>=':
+                        expr = B.core.rel.gre([e1, e2]);
+                        break;
+                    case '<=':
+                        expr = B.core.rel.lte([e1, e2]);
+                        break;
+                    default: throw new Error(`value operator '${op}' not supported`);
+                }
+            }
+            return B.struct.generator.atomGroups({ 'atom-test': expr });
+        }
+    }
+];
+
+const lang = P.MonadicParser.createLanguage({
+    Parens: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Parens,
+            r.Operator,
+            r.Expression
+        ).wrap(P.MonadicParser.string('('), P.MonadicParser.string(')'));
+    },
+
+    Expression: function (r: any) {
+        return P.MonadicParser.alt(
+            r.RangeListProperty,
+            //	    r.NamedAtomProperties,
+            r.ValueQuery,
+            r.Keywords,
+        );
+    },
+
+    NamedAtomProperties: function () {
+        return P.MonadicParser.alt(...h.getNamedPropertyRules(properties));
+    },
+
+    Keywords: () => P.MonadicParser.alt(...h.getKeywordRules(keywords)),
+
+    ValueRange: function (r: any) {
+        return P.MonadicParser.seq(
+            r.Value
+                .skip(P.MonadicParser.regexp(/\s+TO\s+/i)),
+            r.Value
+        ).map(x => ({ range: x }));
+    },
+
+    RangeListProperty: function (r: any) {
+        return P.MonadicParser.seq(
+            P.MonadicParser.alt(...h.getPropertyNameRules(properties, /\s/))
+                .skip(P.MonadicParser.whitespace),
+            P.MonadicParser.alt(
+                r.ValueRange,
+                r.Value
+            ).sepBy1(P.MonadicParser.whitespace)
+        ).map(x => {
+            const [property, values] = x;
+            const listValues: (string | number)[] = [];
+            const rangeValues: any[] = [];
+
+            values.forEach((v: any) => {
+                if (v.range) {
+                    rangeValues.push(
+                        B.core.rel.inRange([property, v.range[0], v.range[1]])
+                    );
+                } else {
+                    listValues.push(h.wrapValue(property, v, sstrucDict));
+                }
+            });
+
+            const rangeTest = h.orExpr(rangeValues);
+            const listTest = h.valuesTest(property, listValues);
+
+            let test;
+            if (rangeTest && listTest) {
+                test = B.core.logic.or([rangeTest, listTest]);
+            } else {
+                test = rangeTest ? rangeTest : listTest;
+            }
+
+            return B.struct.generator.atomGroups({ [h.testLevel(property)]: test });
+        });
+    },
+
+    Operator: function (r: any) {
+        return h.combineOperators(operators, P.MonadicParser.alt(r.Parens, r.Expression, r.ValueQuery));
+    },
+
+    Query: function (r: any) {
+        return P.MonadicParser.alt(
+            r.Operator,
+            r.Parens,
+            r.Expression
+        ).trim(P.MonadicParser.optWhitespace);
+    },
+
+    Number: function () {
+        return P.MonadicParser.regexp(/-?(0|[1-9][0-9]*)([.][0-9]+)?([eE][+-]?[0-9]+)?/)
+            .map(Number)
+            .desc('number');
+    },
+
+    String: function () {
+        const w = h.getReservedWords(properties, keywords, operators)
+            .sort(h.strLenSortFn).map(h.escapeRegExp).join('|');
+        return P.MonadicParser.alt(
+            P.MonadicParser.regexp(new RegExp(`(?!(${w}))[A-Z0-9_]+`, 'i')),
+            P.MonadicParser.regexp(/'((?:[^"\\]|\\.)*)'/, 1),
+            P.MonadicParser.regexp(/"((?:[^"\\]|\\.)*)"/, 1).map((x: any) => B.core.type.regex([`^${x}$`, 'i']))
+        ).desc('string');
+    },
+
+    Value: function (r: any) {
+        return P.MonadicParser.alt(r.Number, r.String);
+    },
+
+    ValueParens: function (r: any) {
+        return P.MonadicParser.alt(
+            r.ValueParens,
+            r.ValueOperator,
+            r.ValueExpressions
+        ).wrap(P.MonadicParser.string('('), P.MonadicParser.string(')'));
+    },
+
+    ValuePropertyNames: function () {
+        return P.MonadicParser.alt(...h.getPropertyNameRules(properties, /=~|==|>=|<=|=|!=|>|<|\)|\s|\+|-|\*|\//i));
+    },
+
+    ValueOperator: function (r: any) {
+        return h.combineOperators(valueOperators, P.MonadicParser.alt(r.ValueParens, r.ValueExpressions));
+    },
+
+    ValueExpressions: function (r: any) {
+        return P.MonadicParser.alt(
+            r.ValueFunctions,
+            r.Value,
+            r.ValuePropertyNames
+        );
+    },
+
+    ValueFunctions: function (r: any) {
+        return P.MonadicParser.alt(...h.getFunctionRules(functions, r.ValueOperator));
+    },
+
+    ValueQuery: function (r: any) {
+        return P.MonadicParser.alt(
+            r.ValueOperator.map((x: any) => {
+                // if (!x.head || x.head.startsWith('core.math') || x.head.startsWith('structure-query.atom-property')) {
+                if (!x.head.name || !x.head.name.startsWith('structure-query.generator')) {
+                    throw new Error(`values must be part of an comparison, value '${x}'`);
+                } else {
+                    return x as any;
+                }
+            })
+        );
+    }
+});
+
+export const transpiler: Transpiler = str => lang.Query.tryParse(str);
diff --git a/src/mol-script/transpilers/vmd/properties.ts b/src/mol-script/transpilers/vmd/properties.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2cf506943f4965f3098b5ce794e3d201896ebd57
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/properties.ts
@@ -0,0 +1,269 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { MolScriptBuilder } from '../../../mol-script/language/builder';
+const B = MolScriptBuilder;
+import { PropertyDict } from '../types';
+
+const reFloat = /[-+]?[0-9]*\.?[0-9]+/;
+const rePosInt = /[+]?[0-9]+/;
+const reInt = /[-+]?[0-9]+/;
+
+function str(x: string) { return x; }
+
+export const sstrucDict: { [key: string]: string } = {
+    T: 'turn', // Turn
+    E: 'sheet', // Extended conformation ($\beta$ sheets)
+    B: 'strand', // Isolated bridge
+    H: 'alpha', // Alpha helix
+    G: '3-10', // 3-10 helix
+    I: 'pi', // Pi helix
+    C: 'none', // Coil
+};
+export function sstrucMap(x: string) {
+    return B.struct.type.secondaryStructureFlags(
+        [sstrucDict[x.toUpperCase()] || 'none']
+    );
+}
+
+export const properties: PropertyDict = {
+    name: {
+        '@desc': 'str    atom name',
+        '@examples': ['name CA'],
+        regex: /[a-zA-Z0-9]+/, map: B.atomName,
+        level: 'atom-test', property: B.ammp('label_atom_id')
+    },
+    type: {
+        '@desc': 'str    atom type',
+        '@examples': ['type C3'],
+        isUnsupported: true,
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'atom-test'
+    },
+    index: {
+        '@desc': 'num    the atom number, starting at 0',
+        '@examples': ['index 10'],
+        isNumeric: true,
+        regex: rePosInt, map: x => (parseInt(x) - 1),
+        level: 'atom-test', property: B.ammp('id')
+    },
+    serial: {
+        '@desc': 'num    the atom number, starting at 1',
+        '@examples': ['serial 11'],
+        isNumeric: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test', property: B.ammp('id')
+    },
+    atomicnumber: {
+        '@desc': 'num    atomic number (0 if undefined)',
+        '@examples': ['atomicnumber 13'],
+        isNumeric: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test', property: B.acp('atomicNumber')
+    },
+    element: {
+        '@desc': 'str  atomic element symbol string ("X" if undefined)',
+        '@examples': ['element N'],
+        regex: /[a-zA-Z0-9]{1,3}/, map: x => B.es(x),
+        level: 'atom-test', property: B.acp('elementSymbol')
+    },
+    altloc: {
+        '@desc': 'str  alternate location/conformation identifier',
+        '@examples': ['altloc C'],
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'atom-test', property: B.ammp('label_alt_id')
+    },
+    chain: {
+        '@desc': 'str  the one-character chain identifier',
+        '@examples': ['chain A'],
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'residue-test', property: B.ammp('auth_asym_id')
+    },
+    residue: {
+        '@desc': 'num  a set of connected atoms with the same residue number',
+        '@examples': ['residue < 11', 'residue 11'],
+        isNumeric: true,
+        regex: reInt, map: x => parseInt(x),
+        level: 'residue-test', property: B.ammp('auth_seq_id')
+    },
+    fragment: {
+        '@desc': 'num  a set of connected residues',
+        '@examples': ['fragment 42'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reInt, map: x => parseInt(x),
+        level: 'residue-test'
+    },
+    pfrag: {
+        '@desc': 'num  a set of connected protein residues',
+        '@examples': ['pfrag 42'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reInt, map: x => parseInt(x),
+        level: 'residue-test'
+    },
+    nfrag: {
+        '@desc': 'num  a set of connected nucleic residues',
+        '@examples': ['nfrag 42'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reInt, map: x => parseInt(x),
+        level: 'residue-test'
+    },
+    sequence: {
+        '@desc': 'str  a sequence given by one letter names',
+        '@examples': ['sequence PGATTACA'],
+        isUnsupported: true,
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'residue-test'
+    },
+    numbonds: {
+        '@desc': 'num  number of bonds',
+        '@examples': ['numbonds = 2', 'numbonds >= 3'],
+        isNumeric: true,
+        regex: rePosInt, map: x => parseInt(x),
+        level: 'atom-test', property: B.acp('bondCount')
+    },
+    resname: {
+        '@desc': 'str  residue name',
+        '@examples': ['resname ALA'],
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'residue-test', property: B.ammp('auth_comp_id')
+    },
+    resid: {
+        '@desc': 'num  residue id',
+        '@examples': ['resid 42'],
+        isNumeric: true,
+        regex: reInt, map: x => parseInt(x),
+        level: 'residue-test', property: B.ammp('auth_seq_id')
+    },
+    segname: {
+        '@desc': 'str  segment name',
+        '@examples': ['segname B'],
+        regex: /[a-zA-Z0-9]+/, map: str,
+        level: 'residue-test', property: B.ammp('label_asym_id')
+    },
+    x: {
+        '@desc': 'float  x coordinate',
+        '@examples': ['x 42'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('x')
+    },
+    y: {
+        '@desc': 'float  y coordinate',
+        '@examples': ['y > 1.7'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('y')
+    },
+    z: {
+        '@desc': 'float  z coordinate',
+        '@examples': ['z < 11', 'z > -21'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('z')
+    },
+    radius: {
+        '@desc': 'float  atomic radius',
+        '@examples': ['radius > 1.3'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('vdw')
+    },
+    mass: {
+        '@desc': 'float  atomic mass',
+        '@examples': ['mass > 2'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.acp('mass')
+    },
+    charge: {
+        '@desc': 'float  atomic charge',
+        '@examples': ['charge > 0', 'charge 1'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('pdbx_formal_charge')
+    },
+    beta: {
+        '@desc': 'float  temperature factor',
+        '@examples': ['beta < 20', 'beta > 35'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('B_iso_or_equiv')
+    },
+    occupancy: {
+        '@desc': 'float  occupancy',
+        '@examples': ['occupancy 1', 'occupancy < 1'],
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test', property: B.ammp('occupancy')
+    },
+    user: {
+        '@desc': 'float  time-varying user-specified value',
+        '@examples': ['user < 0.1'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'atom-test'
+    },
+    rasmol: {
+        '@desc': 'str  translates Rasmol selection string to VMD',
+        '@examples': ["rasmol 'all'"],
+        isUnsupported: true,
+        regex: /[^']*/, map: str,
+        level: 'atom-test'
+    },
+    structure: {
+        '@desc': 'str  single letter name for the secondary structure',
+        '@examples': ['structure H', 'structure H E'],
+        regex: /T|E|B|H|G|I|C/i, map: sstrucMap,
+        level: 'atom-test', property: B.ammp('secondaryStructureFlags')
+    },
+    phi: {
+        '@desc': 'float  phi backbone conformational angles',
+        '@examples': ['phi < 160'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'residue-test'
+    },
+    psi: {
+        '@desc': 'float  psi backbone conformational angles',
+        '@examples': ['psi < 160'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseFloat(x),
+        level: 'residue-test'
+    },
+    ufx: {
+        '@desc': 'num  force to apply in the x coordinate',
+        '@examples': ['ufx 1'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    ufy: {
+        '@desc': 'num  force to apply in the y coordinate',
+        '@examples': ['ufy 1'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+    ufz: {
+        '@desc': 'num  force to apply in the z coordinate',
+        '@examples': ['ufz 1'],
+        isUnsupported: true,
+        isNumeric: true,
+        regex: reFloat, map: x => parseInt(x),
+        level: 'atom-test'
+    },
+};
diff --git a/src/mol-script/transpilers/vmd/symbols.ts b/src/mol-script/transpilers/vmd/symbols.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1740243d4e755c385cf264c915df58456d5d1838
--- /dev/null
+++ b/src/mol-script/transpilers/vmd/symbols.ts
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2017-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author Panagiotis Tourlas <panagiot_tourlov@hotmail.com>
+ *
+ * Adapted from MolQL project
+ */
+
+import { properties } from './properties';
+import { operators } from './operators';
+import { keywords } from './keywords';
+import { functions } from './functions';
+
+export const Properties: string[] = [];
+for (const name in properties) {
+    if (properties[name].isUnsupported) continue;
+    Properties.push(name);
+    if (properties[name].abbr) Properties.push(...properties[name].abbr!);
+}
+
+export const Operators: string[] = [];
+operators.forEach(o => {
+    if (o.isUnsupported) return;
+    Operators.push(o.name);
+    if (o.abbr) Operators.push(...o.abbr);
+});
+
+export const Keywords: string[] = [];
+for (const name in keywords) {
+    if (!keywords[name].map) continue;
+    Keywords.push(name);
+    if (keywords[name].abbr) Keywords.push(...keywords[name].abbr!);
+}
+
+export const Functions: string[] = [];
+for (const name in functions) {
+    if (!functions[name].map) continue;
+    Functions.push(name);
+}
+
+export const all = { Properties, Operators: [...Operators, ...Functions], Keywords };
diff --git a/src/mol-state/action.ts b/src/mol-state/action.ts
index b6416e63f626fa7fd0ebea77692c99c55cd345cf..864a8cb9535468705a5b03ac099e2aa2c443a629 100644
--- a/src/mol-state/action.ts
+++ b/src/mol-state/action.ts
@@ -84,7 +84,7 @@ namespace StateAction {
     }
 
     export namespace Builder {
-        export interface Type<A extends StateObject.Ctor, P extends { }> {
+        export interface Type<A extends StateObject.Ctor, P extends {}> {
             from?: A | A[],
             params?: PD.For<P> | ((a: StateObject.From<A>, globalCtx: any) => PD.For<P>),
             display?: string | { name: string, description?: string },
@@ -95,7 +95,7 @@ namespace StateAction {
             <A extends StateObject.Ctor, P extends { }>(info: Type<A, P>): Define<StateObject.From<A>, PD.Normalize<P>>
         }
 
-        export interface Define<A extends StateObject, P> {
+        export interface Define<A extends StateObject, P extends {}> {
             <T>(def: DefinitionBase<A, T, P> | DefinitionBase<A, T, P>['run']): StateAction<A, T, P>,
         }
 
diff --git a/src/mol-state/transformer.ts b/src/mol-state/transformer.ts
index 0656babb8f3a526776075368a9ca22d45b6b2b41..94905d9af53e6b852a40a47dbe9e46606eb7fd69 100644
--- a/src/mol-state/transformer.ts
+++ b/src/mol-state/transformer.ts
@@ -196,7 +196,7 @@ namespace Transformer {
     }
 
     export namespace Builder {
-        export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }> {
+        export interface Type<A extends StateObject.Ctor, B extends StateObject.Ctor, P extends {}> {
             name: string,
             from: A | A[],
             to: B | B[],
@@ -210,7 +210,7 @@ namespace Transformer {
             <A extends StateObject.Ctor, B extends StateObject.Ctor, P extends { }>(info: Type<A, B, P>): Define<StateObject.From<A>, StateObject.From<B>, PD.Normalize<P>>
         }
 
-        export interface Define<A extends StateObject, B extends StateObject, P> {
+        export interface Define<A extends StateObject, B extends StateObject, P extends {}> {
             (def: DefinitionBase<A, B, P>): Transformer<A, B, P>
         }
 
diff --git a/src/mol-theme/color.ts b/src/mol-theme/color.ts
index 928137db106fe1c31ef1b577715e149a9e1c5857..84af7dad2d2a34f23cfcb4f270fc068dda79f949 100644
--- a/src/mol-theme/color.ts
+++ b/src/mol-theme/color.ts
@@ -28,7 +28,7 @@ import { UncertaintyColorThemeProvider } from './color/uncertainty';
 import { EntitySourceColorThemeProvider } from './color/entity-source';
 import { IllustrativeColorThemeProvider } from './color/illustrative';
 import { HydrophobicityColorThemeProvider } from './color/hydrophobicity';
-import { ModelIndexColorThemeProvider } from './color/model-index';
+import { TrajectoryIndexColorThemeProvider } from './color/trajectory-index';
 import { OccupancyColorThemeProvider } from './color/occupancy';
 import { OperatorNameColorThemeProvider } from './color/operator-name';
 import { OperatorHklColorThemeProvider } from './color/operator-hkl';
@@ -38,6 +38,8 @@ import { EntityIdColorThemeProvider } from './color/entity-id';
 import { Texture, TextureFilter } from '../mol-gl/webgl/texture';
 import { VolumeValueColorThemeProvider } from './color/volume-value';
 import { Vec3, Vec4 } from '../mol-math/linear-algebra';
+import { ModelIndexColorThemeProvider } from './color/model-index';
+import { StructureIndexColorThemeProvider } from './color/structure-index';
 
 export type LocationColor = (location: Location, isSecondary: boolean) => Color
 
@@ -144,6 +146,8 @@ namespace ColorTheme {
         'secondary-structure': SecondaryStructureColorThemeProvider,
         'sequence-id': SequenceIdColorThemeProvider,
         'shape-group': ShapeGroupColorThemeProvider,
+        'structure-index': StructureIndexColorThemeProvider,
+        'trajectory-index': TrajectoryIndexColorThemeProvider,
         'uncertainty': UncertaintyColorThemeProvider,
         'unit-index': UnitIndexColorThemeProvider,
         'uniform': UniformColorThemeProvider,
diff --git a/src/mol-theme/color/element-symbol.ts b/src/mol-theme/color/element-symbol.ts
index bb75aa34d3b42175020cb7dd2eb7a867ef1dc0a3..a466848df48e6d5b9b41e5ffc1d4a6e30954eed2 100644
--- a/src/mol-theme/color/element-symbol.ts
+++ b/src/mol-theme/color/element-symbol.ts
@@ -19,6 +19,8 @@ import { OperatorNameColorThemeParams, OperatorNameColorTheme } from './operator
 import { EntityIdColorTheme, EntityIdColorThemeParams } from './entity-id';
 import { assertUnreachable } from '../../mol-util/type-helpers';
 import { EntitySourceColorTheme, EntitySourceColorThemeParams } from './entity-source';
+import { ModelIndexColorTheme, ModelIndexColorThemeParams } from './model-index';
+import { StructureIndexColorTheme, StructureIndexColorThemeParams } from './structure-index';
 
 // from Jmol http://jmol.sourceforge.net/jscolors/ (or 0xFFFFFF)
 export const ElementSymbolColors = ColorMap({
@@ -35,6 +37,8 @@ export const ElementSymbolColorThemeParams = {
         'entity-id': PD.Group(EntityIdColorThemeParams),
         'entity-source': PD.Group(EntitySourceColorThemeParams),
         'operator-name': PD.Group(OperatorNameColorThemeParams),
+        'model-index': PD.Group(ModelIndexColorThemeParams),
+        'structure-index': PD.Group(StructureIndexColorThemeParams),
         'element-symbol': PD.EmptyGroup()
     }, { description: 'Use chain-id coloring for carbon atoms.' }),
     saturation: PD.Numeric(0, { min: -6, max: 6, step: 0.1 }),
@@ -46,7 +50,7 @@ export const ElementSymbolColorThemeParams = {
 };
 export type ElementSymbolColorThemeParams = typeof ElementSymbolColorThemeParams
 export function getElementSymbolColorThemeParams(ctx: ThemeDataContext) {
-    return ElementSymbolColorThemeParams; // TODO return copy
+    return PD.clone(ElementSymbolColorThemeParams);
 }
 
 export function elementSymbolColor(colorMap: ElementSymbolColors, element: ElementSymbol): Color {
@@ -63,8 +67,10 @@ export function ElementSymbolColorTheme(ctx: ThemeDataContext, props: PD.Values<
             pcc.name === 'entity-id' ? EntityIdColorTheme(ctx, pcc.params).color :
                 pcc.name === 'entity-source' ? EntitySourceColorTheme(ctx, pcc.params).color :
                     pcc.name === 'operator-name' ? OperatorNameColorTheme(ctx, pcc.params).color :
-                        pcc.name === 'element-symbol' ? undefined :
-                            assertUnreachable(pcc);
+                        pcc.name === 'model-index' ? ModelIndexColorTheme(ctx, pcc.params).color :
+                            pcc.name === 'structure-index' ? StructureIndexColorTheme(ctx, pcc.params).color :
+                                pcc.name === 'element-symbol' ? undefined :
+                                    assertUnreachable(pcc);
 
     function elementColor(element: ElementSymbol, location: Location) {
         return (carbonColor && element === 'C')
diff --git a/src/mol-theme/color/illustrative.ts b/src/mol-theme/color/illustrative.ts
index 7483f5d4b92d3426a59d879f8f0ccae142bb6360..18510ef68fdb85d0aa57c6d49ae08eefeae2009c 100644
--- a/src/mol-theme/color/illustrative.ts
+++ b/src/mol-theme/color/illustrative.ts
@@ -17,9 +17,11 @@ import { assertUnreachable } from '../../mol-util/type-helpers';
 import { EntityIdColorTheme, EntityIdColorThemeParams } from './entity-id';
 import { MoleculeTypeColorTheme, MoleculeTypeColorThemeParams } from './molecule-type';
 import { EntitySourceColorTheme, EntitySourceColorThemeParams } from './entity-source';
+import { ModelIndexColorTheme, ModelIndexColorThemeParams } from './model-index';
+import { StructureIndexColorTheme, StructureIndexColorThemeParams } from './structure-index';
 
 const DefaultIllustrativeColor = Color(0xEEEEEE);
-const Description = `Assigns an illustrative color that gives every chain a color based on the choosen style but with lighter carbons (inspired by David Goodsell's Molecule of the Month style).`;
+const Description = `Assigns an illustrative color that gives every chain a color based on the chosen style but with lighter carbons (inspired by David Goodsell's Molecule of the Month style).`;
 
 export const IllustrativeColorThemeParams = {
     style: PD.MappedStatic('entity-id', {
@@ -28,6 +30,8 @@ export const IllustrativeColorThemeParams = {
         'entity-id': PD.Group(EntityIdColorThemeParams),
         'entity-source': PD.Group(EntitySourceColorThemeParams),
         'molecule-type': PD.Group(MoleculeTypeColorThemeParams),
+        'model-index': PD.Group(ModelIndexColorThemeParams),
+        'structure-index': PD.Group(StructureIndexColorThemeParams),
     }),
     carbonLightness: PD.Numeric(0.8, { min: -6, max: 6, step: 0.1 })
 };
@@ -44,7 +48,9 @@ export function IllustrativeColorTheme(ctx: ThemeDataContext, props: PD.Values<I
                 props.style.name === 'entity-id' ? EntityIdColorTheme(ctx, props.style.params) :
                     props.style.name === 'entity-source' ? EntitySourceColorTheme(ctx, props.style.params) :
                         props.style.name === 'molecule-type' ? MoleculeTypeColorTheme(ctx, props.style.params) :
-                            assertUnreachable(props.style);
+                            props.style.name === 'model-index' ? ModelIndexColorTheme(ctx, props.style.params) :
+                                props.style.name === 'structure-index' ? StructureIndexColorTheme(ctx, props.style.params) :
+                                    assertUnreachable(props.style);
 
     function illustrativeColor(location: Location, typeSymbol: ElementSymbol) {
         const baseColor = styleColor(location, false);
diff --git a/src/mol-theme/color/model-index.ts b/src/mol-theme/color/model-index.ts
index 51e2b4ea98ab4920a109dfad83e5f685394390dd..cbdfd683f10abebf07ceee1ba180b57331440ce0 100644
--- a/src/mol-theme/color/model-index.ts
+++ b/src/mol-theme/color/model-index.ts
@@ -1,6 +1,7 @@
 /**
- * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
+ * @author Jason Pattle <jpattle@exscientia.co.uk>
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
@@ -14,14 +15,14 @@ import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
 import { TableLegend, ScaleLegend } from '../../mol-util/legend';
 
 const DefaultColor = Color(0xCCCCCC);
-const Description = 'Gives every model a unique color based on the position (index) of the model in the list of models in the structure.';
+const Description = 'Gives every model a unique color based on its index.';
 
 export const ModelIndexColorThemeParams = {
-    ...getPaletteParams({ type: 'colors', colorList: 'purples' }),
+    ...getPaletteParams({ type: 'colors', colorList: 'many-distinct' }),
 };
 export type ModelIndexColorThemeParams = typeof ModelIndexColorThemeParams
 export function getModelIndexColorThemeParams(ctx: ThemeDataContext) {
-    return ModelIndexColorThemeParams; // TODO return copy
+    return PD.clone(ModelIndexColorThemeParams);
 }
 
 export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<ModelIndexColorThemeParams>): ColorTheme<ModelIndexColorThemeParams> {
@@ -29,24 +30,17 @@ export function ModelIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<Mod
     let legend: ScaleLegend | TableLegend | undefined;
 
     if (ctx.structure) {
-        const { models } = ctx.structure.root;
-
-        let size = 0;
-        for (const m of models) size = Math.max(size, Model.TrajectoryInfo.get(m)?.size || 0);
+        // max-index is the same for all models
+        const size = (Model.MaxIndex.get(ctx.structure.models[0]).value ?? -1) + 1;
 
         const palette = getPalette(size, props);
         legend = palette.legend;
-        const modelColor = new Map<number, Color>();
-        for (let i = 0, il = models.length; i < il; ++i) {
-            const idx = Model.TrajectoryInfo.get(models[i])?.index || 0;
-            modelColor.set(idx, palette.color(idx));
-        }
 
         color = (location: Location): Color => {
             if (StructureElement.Location.is(location)) {
-                return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
+                return palette.color(Model.Index.get(location.unit.model).value || 0)!;
             } else if (Bond.isLocation(location)) {
-                return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
+                return palette.color(Model.Index.get(location.aUnit.model).value || 0)!;
             }
             return DefaultColor;
         };
@@ -71,5 +65,5 @@ export const ModelIndexColorThemeProvider: ColorTheme.Provider<ModelIndexColorTh
     factory: ModelIndexColorTheme,
     getParams: getModelIndexColorThemeParams,
     defaultValues: PD.getDefaultValues(ModelIndexColorThemeParams),
-    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.elementCount > 0 && Model.TrajectoryInfo.get(ctx.structure.models[0]).size > 1
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.elementCount > 0
 };
\ No newline at end of file
diff --git a/src/mol-theme/color/structure-index.ts b/src/mol-theme/color/structure-index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..28dda92802ae4db29fd67dc7f807fc3ec1988583
--- /dev/null
+++ b/src/mol-theme/color/structure-index.ts
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color } from '../../mol-util/color';
+import { Location } from '../../mol-model/location';
+import { StructureElement, Bond, Structure } from '../../mol-model/structure';
+import { ColorTheme, LocationColor } from '../color';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ThemeDataContext } from '../../mol-theme/theme';
+import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
+import { TableLegend, ScaleLegend } from '../../mol-util/legend';
+
+const DefaultColor = Color(0xCCCCCC);
+const Description = 'Gives every structure a unique color based on its index.';
+
+export const StructureIndexColorThemeParams = {
+    ...getPaletteParams({ type: 'colors', colorList: 'many-distinct' }),
+};
+export type StructureIndexColorThemeParams = typeof StructureIndexColorThemeParams
+export function getStructureIndexColorThemeParams(ctx: ThemeDataContext) {
+    return PD.clone(StructureIndexColorThemeParams);
+}
+
+export function StructureIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<StructureIndexColorThemeParams>): ColorTheme<StructureIndexColorThemeParams> {
+    let color: LocationColor;
+    let legend: ScaleLegend | TableLegend | undefined;
+
+    if (ctx.structure) {
+        const size = (Structure.MaxIndex.get(ctx.structure).value ?? -1) + 1;
+
+        const palette = getPalette(size, props);
+        legend = palette.legend;
+
+        color = (location: Location): Color => {
+            if (StructureElement.Location.is(location)) {
+                return palette.color(Structure.Index.get(location.structure).value || 0)!;
+            } else if (Bond.isLocation(location)) {
+                return palette.color(Structure.Index.get(location.aStructure).value || 0)!;
+            }
+            return DefaultColor;
+        };
+    } else {
+        color = () => DefaultColor;
+    }
+
+    return {
+        factory: StructureIndexColorTheme,
+        granularity: 'instance',
+        color,
+        props,
+        description: Description,
+        legend
+    };
+}
+
+export const StructureIndexColorThemeProvider: ColorTheme.Provider<StructureIndexColorThemeParams, 'structure-index'> = {
+    name: 'structure-index',
+    label: 'Structure Index',
+    category: ColorTheme.Category.Chain,
+    factory: StructureIndexColorTheme,
+    getParams: getStructureIndexColorThemeParams,
+    defaultValues: PD.getDefaultValues(StructureIndexColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.elementCount > 0
+};
\ No newline at end of file
diff --git a/src/mol-theme/color/trajectory-index.ts b/src/mol-theme/color/trajectory-index.ts
new file mode 100644
index 0000000000000000000000000000000000000000..87b1b995518805448bdffc7baab9d46447c3ef26
--- /dev/null
+++ b/src/mol-theme/color/trajectory-index.ts
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2019-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Color } from '../../mol-util/color';
+import { Location } from '../../mol-model/location';
+import { StructureElement, Bond, Model } from '../../mol-model/structure';
+import { ColorTheme, LocationColor } from '../color';
+import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ThemeDataContext } from '../theme';
+import { getPaletteParams, getPalette } from '../../mol-util/color/palette';
+import { TableLegend, ScaleLegend } from '../../mol-util/legend';
+
+const DefaultColor = Color(0xCCCCCC);
+const Description = 'Gives every model (frame) a unique color based on the index in its trajectory.';
+
+export const TrajectoryIndexColorThemeParams = {
+    ...getPaletteParams({ type: 'colors', colorList: 'purples' }),
+};
+export type TrajectoryIndexColorThemeParams = typeof TrajectoryIndexColorThemeParams
+export function getTrajectoryIndexColorThemeParams(ctx: ThemeDataContext) {
+    return PD.clone(TrajectoryIndexColorThemeParams);
+}
+
+export function TrajectoryIndexColorTheme(ctx: ThemeDataContext, props: PD.Values<TrajectoryIndexColorThemeParams>): ColorTheme<TrajectoryIndexColorThemeParams> {
+    let color: LocationColor;
+    let legend: ScaleLegend | TableLegend | undefined;
+
+    if (ctx.structure) {
+        const { models } = ctx.structure.root;
+
+        let size = 0;
+        for (const m of models) size = Math.max(size, Model.TrajectoryInfo.get(m)?.size || 0);
+
+        const palette = getPalette(size, props);
+        legend = palette.legend;
+        const modelColor = new Map<number, Color>();
+        for (let i = 0, il = models.length; i < il; ++i) {
+            const idx = Model.TrajectoryInfo.get(models[i])?.index || 0;
+            modelColor.set(idx, palette.color(idx));
+        }
+
+        color = (location: Location): Color => {
+            if (StructureElement.Location.is(location)) {
+                return modelColor.get(Model.TrajectoryInfo.get(location.unit.model).index)!;
+            } else if (Bond.isLocation(location)) {
+                return modelColor.get(Model.TrajectoryInfo.get(location.aUnit.model).index)!;
+            }
+            return DefaultColor;
+        };
+    } else {
+        color = () => DefaultColor;
+    }
+
+    return {
+        factory: TrajectoryIndexColorTheme,
+        granularity: 'instance',
+        color,
+        props,
+        description: Description,
+        legend
+    };
+}
+
+export const TrajectoryIndexColorThemeProvider: ColorTheme.Provider<TrajectoryIndexColorThemeParams, 'trajectory-index'> = {
+    name: 'trajectory-index',
+    label: 'Trajectory Index',
+    category: ColorTheme.Category.Chain,
+    factory: TrajectoryIndexColorTheme,
+    getParams: getTrajectoryIndexColorThemeParams,
+    defaultValues: PD.getDefaultValues(TrajectoryIndexColorThemeParams),
+    isApplicable: (ctx: ThemeDataContext) => !!ctx.structure && ctx.structure.elementCount > 0 && Model.TrajectoryInfo.get(ctx.structure.models[0]).size > 1
+};
\ No newline at end of file
diff --git a/src/mol-util/_spec/fibonacci-heap.spec.ts b/src/mol-util/_spec/fibonacci-heap.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4016e8faf9655ff72f6d95f01174fa0798cd55be
--- /dev/null
+++ b/src/mol-util/_spec/fibonacci-heap.spec.ts
@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ */
+
+import { FibonacciHeap } from '../fibonacci-heap';
+
+describe('fibonacci-heap', () => {
+    it('basic', () => {
+        const heap = new FibonacciHeap();
+        heap.insert(1, 2);
+        heap.insert(4);
+        heap.insert(2);
+        heap.insert(3);
+        expect(heap.size()).toBe(4);
+        const node = heap.extractMinimum();
+        expect(node!.key).toBe(1);
+        expect(node!.value).toBe(2);
+        expect(heap.size()).toBe(3);
+    });
+});
diff --git a/src/mol-util/binding.ts b/src/mol-util/binding.ts
index 3ca2e97da24ad2e4f94087fb2aef71ab9b8c5d80..252de33a3bdd563565fe216d7e704486c9740d51 100644
--- a/src/mol-util/binding.ts
+++ b/src/mol-util/binding.ts
@@ -24,6 +24,10 @@ namespace Binding {
         return { triggers, action, description };
     }
 
+    export function isBinding(x: any): x is Binding {
+        return !!x && Array.isArray(x.triggers) && typeof x.action === 'string';
+    }
+
     export const Empty: Binding = { triggers: [], action: '', description: '' };
     export function isEmpty(binding: Binding) {
         return binding.triggers.length === 0 ||
diff --git a/src/mol-util/color/distinct.ts b/src/mol-util/color/distinct.ts
index c12a8c9e26b873171ba1c36b0e2c71c4eee62abd..e2e3c1507513798dab052a6e9d23a56828973d60 100644
--- a/src/mol-util/color/distinct.ts
+++ b/src/mol-util/color/distinct.ts
@@ -13,6 +13,7 @@ import { deepClone } from '../../mol-util/object';
 import { deepEqual } from '../../mol-util';
 import { arraySum } from '../../mol-util/array';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { ColorNames } from './names';
 
 export const DistinctColorsParams = {
     hue: PD.Interval([1, 360], { min: 0, max: 360, step: 1 }),
@@ -105,7 +106,8 @@ export function distinctColors(count: number, props: Partial<DistinctColorsProps
 
     const samples = getSamples(Math.max(p.minSampleCount, count * 5), p);
     if (samples.length < count) {
-        throw new Error('Not enough samples to generate distinct colors, increase sample count.');
+        console.warn('Not enough samples to generate distinct colors, increase sample count.');
+        return (new Array(count)).fill(ColorNames.lightgrey);
     }
 
     const colors: Lab[] = [];
diff --git a/src/mol-util/fibonacci-heap.ts b/src/mol-util/fibonacci-heap.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5602bd4b837117db57bed611d2ae282fa2e8fb33
--- /dev/null
+++ b/src/mol-util/fibonacci-heap.ts
@@ -0,0 +1,407 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Gianluca Tomasello <giagitom@gmail.com>
+ *
+ * Adapted from https://github.com/gwtw/ts-fibonacci-heap, Copyright (c) 2014 Daniel Imms, MIT
+ */
+
+interface INode<K, V> {
+    key: K;
+    value?: V;
+}
+
+type CompareFunction<K, V> = (a: INode<K, V>, b: INode<K, V>) => number;
+
+class Node<K, V> implements INode<K, V> {
+    public key: K;
+    public value: V | undefined;
+    public prev: Node<K, V>;
+    public next: Node<K, V>;
+    public parent: Node<K, V> | null = null;
+    public child: Node<K, V> | null = null;
+
+    public degree: number = 0;
+    public isMarked: boolean = false;
+
+    constructor(key: K, value?: V) {
+        this.key = key;
+        this.value = value;
+        this.prev = this;
+        this.next = this;
+    }
+}
+
+class NodeListIterator<K, V> {
+    private _index: number;
+    private _items: Node<K, V>[];
+    private _len: number;
+    /**
+   * Creates an Iterator used to simplify the consolidate() method. It works by
+   * making a shallow copy of the nodes in the root list and iterating over the
+   * shallow copy instead of the source as the source will be modified.
+   * @param start A node from the root list.
+   */
+    constructor(start?: Node<K, V>) {
+        this._index = -1;
+        this._items = [];
+        this._len = 0;
+        if (start) {
+            let current = start, l = 0;
+            do {
+                this._items[l++] = current;
+                current = current.next;
+            } while (start !== current);
+            this._len = l;
+        }
+    }
+
+    /**
+   * @return Whether there is a next node in the iterator.
+   */
+    public hasNext(): boolean {
+        return this._index < this._len - 1;
+    }
+
+    /**
+   * @return The next node.
+   */
+    public next(): Node<K, V> {
+        return this._items[++this._index];
+    }
+
+    /**
+   * @return Resets iterator to reuse it.
+   */
+    public reset(start: Node<K, V>) {
+        this._index = -1;
+        this._len = 0;
+        let current = start, l = 0;
+        do {
+            this._items[l++] = current;
+            current = current.next;
+        } while (start !== current);
+        this._len = l;
+    }
+}
+
+const tmpIt = new NodeListIterator<any, any>();
+/**
+ * A Fibonacci heap data structure with a key and optional value.
+*/
+export class FibonacciHeap<K, V> {
+    private _minNode: Node<K, V> | null = null;
+    private _nodeCount: number = 0;
+    private _compare: CompareFunction<K, V>;
+
+    constructor(
+        compare?: CompareFunction<K, V>
+    ) {
+        this._compare = compare ? compare : this._defaultCompare;
+    }
+
+    /**
+   * Clears the heap's data, making it an empty heap.
+   */
+    public clear(): void {
+        this._minNode = null;
+        this._nodeCount = 0;
+    }
+
+    /**
+   * Decreases a key of a node.
+   * @param node The node to decrease the key of.
+   * @param newKey The new key to assign to the node.
+   */
+    public decreaseKey(node: Node<K, V>, newKey: K): void {
+        if (!node) {
+            throw new Error('Cannot decrease key of non-existent node');
+        }
+        if (this._compare({ key: newKey }, { key: node.key }) > 0) {
+            throw new Error('New key is larger than old key');
+        }
+
+        node.key = newKey;
+        const parent = node.parent;
+        if (parent && this._compare(node, parent) < 0) {
+            this._cut(node, parent, <Node<K, V>> this._minNode);
+            this._cascadingCut(parent, <Node<K, V>> this._minNode);
+        }
+        if (this._compare(node, <Node<K, V>> this._minNode) < 0) {
+            this._minNode = node;
+        }
+    }
+
+    /**
+   * Deletes a node.
+   * @param node The node to delete.
+   */
+    public delete(node: Node<K, V>): void {
+    // This is a special implementation of decreaseKey that sets the argument to
+    // the minimum value. This is necessary to make generic keys work, since there
+    // is no MIN_VALUE constant for generic types.
+        const parent = node.parent;
+        if (parent) {
+            this._cut(node, parent, <Node<K, V>> this._minNode);
+            this._cascadingCut(parent, <Node<K, V>> this._minNode);
+        }
+        this._minNode = node;
+
+        this.extractMinimum();
+    }
+
+    /**
+   * Extracts and returns the minimum node from the heap.
+   * @return The heap's minimum node or null if the heap is empty.
+   */
+    public extractMinimum(): Node<K, V> | null {
+        const extractedMin = this._minNode;
+        if (extractedMin) {
+            // Set parent to null for the minimum's children
+            if (extractedMin.child) {
+                let child = extractedMin.child;
+                do {
+                    child.parent = null;
+                    child = child.next;
+                } while (child !== extractedMin.child);
+            }
+
+            let nextInRootList = null;
+            if (extractedMin.next !== extractedMin) {
+                nextInRootList = extractedMin.next;
+            }
+            // Remove min from root list
+            this._removeNodeFromList(extractedMin);
+            this._nodeCount--;
+
+            // Merge the children of the minimum node with the root list
+            this._minNode = this._mergeLists(nextInRootList, extractedMin.child);
+            if (this._minNode) {
+                this._minNode = this._consolidate(this._minNode);
+            }
+        }
+        return extractedMin;
+    }
+
+    /**
+   * Returns the minimum node from the heap.
+   * @return The heap's minimum node or null if the heap is empty.
+   */
+    public findMinimum(): Node<K, V> | null {
+        return this._minNode;
+    }
+
+    /**
+   * Inserts a new key-value pair into the heap.
+   * @param key The key to insert.
+   * @param value The value to insert.
+   * @return node The inserted node.
+   */
+    public insert(key: K, value?: V): Node<K, V> {
+        const node = new Node(key, value);
+        this._minNode = this._mergeLists(this._minNode, node);
+        this._nodeCount++;
+        return node;
+    }
+
+    /**
+   * @return Whether the heap is empty.
+   */
+    public isEmpty(): boolean {
+        return this._minNode === null;
+    }
+
+    /**
+   * @return The size of the heap.
+   */
+    public size(): number {
+        if (this._minNode === null) {
+            return 0;
+        }
+        return this._getNodeListSize(this._minNode);
+    }
+
+    /**
+   * Joins another heap to this heap.
+   * @param other The other heap.
+   */
+    public union(other: FibonacciHeap<K, V>): void {
+        this._minNode = this._mergeLists(this._minNode, other._minNode);
+        this._nodeCount += other._nodeCount;
+    }
+
+    /**
+   * Compares two nodes with each other.
+   * @param a The first key to compare.
+   * @param b The second key to compare.
+   * @return -1, 0 or 1 if a < b, a == b or a > b respectively.
+   */
+    private _defaultCompare(a: INode<K, V>, b: INode<K, V>): number {
+        if (a.key > b.key) {
+            return 1;
+        }
+        if (a.key < b.key) {
+            return -1;
+        }
+        return 0;
+    }
+
+    /**
+   * Cut the link between a node and its parent, moving the node to the root list.
+   * @param node The node being cut.
+   * @param parent The parent of the node being cut.
+   * @param minNode The minimum node in the root list.
+   * @return The heap's new minimum node.
+   */
+    private _cut(node: Node<K, V>, parent: Node<K, V>, minNode: Node<K, V>): Node<K, V> | null {
+        node.parent = null;
+        parent.degree--;
+        if (node.next === node) {
+            parent.child = null;
+        } else {
+            parent.child = node.next;
+        }
+        this._removeNodeFromList(node);
+        const newMinNode = this._mergeLists(minNode, node);
+        node.isMarked = false;
+        return newMinNode;
+    }
+
+    /**
+   * Perform a cascading cut on a node; mark the node if it is not marked,
+   * otherwise cut the node and perform a cascading cut on its parent.
+   * @param node The node being considered to be cut.
+   * @param minNode The minimum node in the root list.
+   * @return The heap's new minimum node.
+   */
+    private _cascadingCut(node: Node<K, V>, minNode: Node<K, V> | null): Node<K, V> | null {
+        const parent = node.parent;
+        if (parent) {
+            if (node.isMarked) {
+                minNode = this._cut(node, parent, <Node<K, V>>minNode);
+                minNode = this._cascadingCut(parent, minNode);
+            } else {
+                node.isMarked = true;
+            }
+        }
+        return minNode;
+    }
+
+    /**
+   * Merge all trees of the same order together until there are no two trees of
+   * the same order.
+   * @param minNode The current minimum node.
+   * @return The new minimum node.
+   */
+    private _consolidate(minNode: Node<K, V>): Node<K, V> | null {
+
+        const aux = [];
+        tmpIt.reset(minNode);
+        while (tmpIt.hasNext()) {
+            let current = tmpIt.next();
+
+            // If there exists another node with the same degree, merge them
+            let auxCurrent = aux[current.degree];
+            while (auxCurrent) {
+                if (this._compare(current, auxCurrent) > 0) {
+                    const temp = current;
+                    current = auxCurrent;
+                    auxCurrent = temp;
+                }
+                this._linkHeaps(auxCurrent, current);
+                aux[current.degree] = null;
+                current.degree++;
+                auxCurrent = aux[current.degree];
+            }
+
+            aux[current.degree] = current;
+        }
+
+        let newMinNode = null;
+        for (let i = 0; i < aux.length; i++) {
+            const node = aux[i];
+            if (node) {
+                // Remove siblings before merging
+                node.next = node;
+                node.prev = node;
+                newMinNode = this._mergeLists(newMinNode, node);
+            }
+        }
+        return newMinNode;
+    }
+
+    /**
+   * Removes a node from a node list.
+   * @param node The node to remove.
+   */
+    private _removeNodeFromList(node: Node<K, V>): void {
+        const prev = node.prev;
+        const next = node.next;
+        prev.next = next;
+        next.prev = prev;
+        node.next = node;
+        node.prev = node;
+    }
+
+    /**
+   * Links two heaps of the same order together.
+   *
+   * @private
+   * @param max The heap with the larger root.
+   * @param min The heap with the smaller root.
+   */
+    private _linkHeaps(max: Node<K, V>, min: Node<K, V>): void {
+        this._removeNodeFromList(max);
+        min.child = this._mergeLists(max, min.child);
+        max.parent = min;
+        max.isMarked = false;
+    }
+
+    /**
+   * Merge two lists of nodes together.
+   *
+   * @private
+   * @param a The first list to merge.
+   * @param b The second list to merge.
+   * @return The new minimum node from the two lists.
+   */
+    private _mergeLists(a: Node<K, V> | null, b: Node<K, V> | null): Node<K, V> | null {
+        if (!a) {
+            if (!b) {
+                return null;
+            }
+            return b;
+        }
+        if (!b) {
+            return a;
+        }
+
+        const temp = a.next;
+        a.next = b.next;
+        a.next.prev = a;
+        b.next = temp;
+        b.next.prev = b;
+
+        return this._compare(a, b) < 0 ? a : b;
+    }
+
+    /**
+   * Gets the size of a node list.
+   * @param node A node within the node list.
+   * @return The size of the node list.
+   */
+    private _getNodeListSize(node: Node<K, V>): number {
+        let count = 0;
+        let current = node;
+
+        do {
+            count++;
+            if (current.child) {
+                count += this._getNodeListSize(current.child);
+            }
+            current = current.next;
+        } while (current !== node);
+
+        return count;
+    }
+}
diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts
index 4db05201cd2b0631b5534f3be008456b440a42bf..ba4d557c3552354033770c9b3fe0cebe29fc33fa 100644
--- a/src/mol-util/index.ts
+++ b/src/mol-util/index.ts
@@ -117,7 +117,7 @@ export function defaults<T>(value: T | undefined, defaultValue: T): T {
     return value !== undefined ? value : defaultValue;
 }
 
-export function extend<S, T, U>(object: S, source: T, guard?: U): S & T & U {
+export function extend<S extends {}, T extends {}, U extends {}>(object: S, source: T, guard?: U): S & T & U {
     let v: any;
 
     const s = <any>source;
@@ -139,7 +139,7 @@ export function extend<S, T, U>(object: S, source: T, guard?: U): S & T & U {
     return <any>object;
 }
 
-export function shallowClone<T>(o: T): T {
+export function shallowClone<T extends {}>(o: T): T {
     return extend({}, o) as T;
 }
 
@@ -158,7 +158,7 @@ function _assign<T>(target: T): T {
 export declare function _assignType<T>(o: T, ...from: any[]): T;
 export const assign: (<T>(o: T, ...from: any[]) => T) = (Object as any).assign || _assign;
 
-function _shallowMerge1<T>(source: T, update: T) {
+function _shallowMerge1<T extends {}>(source: T, update: T) {
     let changed = false;
     for (const k of Object.keys(update)) {
         if (!hasOwnProperty.call(update, k)) continue;
diff --git a/src/mol-util/input/input-observer.ts b/src/mol-util/input/input-observer.ts
index 9326d9f077e07a94f2444f0e90aebdb92f73dc0a..56154c53140305edf15cfe35d05c55ec492b2930 100644
--- a/src/mol-util/input/input-observer.ts
+++ b/src/mol-util/input/input-observer.ts
@@ -304,6 +304,7 @@ namespace InputObserver {
         let buttons = ButtonsType.create(ButtonsType.Flag.None);
         let button = ButtonsType.Flag.None;
         let isInside = false;
+        let hasMoved = false;
 
         const events = createEvents();
         const { drag, interactionEnd, wheel, pinch, gesture, click, move, leave, enter, resize, modifiers, key } = events;
@@ -577,12 +578,13 @@ namespace InputObserver {
             if (!mask(ev.clientX, ev.clientY)) return;
 
             eventOffset(pointerEnd, ev);
-            if (Vec2.distance(pointerEnd, pointerDown) < 4) {
+            if (!hasMoved && Vec2.distance(pointerEnd, pointerDown) < 4) {
                 const { pageX, pageY } = ev;
                 const [x, y] = pointerEnd;
 
                 click.next({ x, y, pageX, pageY, buttons, button, modifiers: getModifierKeys() });
             }
+            hasMoved = false;
         }
 
         function onPointerMove(ev: PointerEvent) {
@@ -604,6 +606,10 @@ namespace InputObserver {
             const isStart = dragging === DraggingState.Started;
             if (isStart && !mask(ev.clientX, ev.clientY)) return;
 
+            if (Vec2.distance(pointerEnd, pointerDown) >= 4) {
+                hasMoved = true;
+            }
+
             const [dx, dy] = pointerDelta;
             drag.next({ x, y, dx, dy, pageX, pageY, buttons, button, modifiers: getModifierKeys(), isStart });
 
diff --git a/src/mol-util/monadic-parser.ts b/src/mol-util/monadic-parser.ts
index 0bf8004d303558b95bc2c707cf75e7c3b66763d0..ad96df44cd41b194f46ddba1993e30359ffea7d6 100644
--- a/src/mol-util/monadic-parser.ts
+++ b/src/mol-util/monadic-parser.ts
@@ -2,12 +2,11 @@
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-/**
+ * @author Koya Sakuma <koya.sakuma.work@gmail.com>
+ **
  * Adapted from Parsimmon (https://github.com/jneen/parsimmon)
  * Copyright (c) 2011-present J. Adkisson (http://jneen.net).
- */
+ **/
 
 export class MonadicParser<A> {
     constructor(public _: MonadicParser.Action<A>) { }
@@ -234,15 +233,27 @@ export namespace MonadicParser {
 
     export type Result<T> = Success<T> | Failure
 
-    // export function createLanguage(parsers: any) {
-    //     const language: any = {};
-    //     for (const key of Object.keys(parsers)) {
-    //         (function (key) {
-    //             language[key] = lazy(() => parsers[key](language));
-    //         })(key);
-    //     }
-    //     return language;
-    // }
+    export function seqMap<A, B>(a: MonadicParser<A>, b: MonadicParser<B>, c: any) {
+        const args = [].slice.call(arguments);
+        if (args.length === 0) {
+            throw new Error('seqMap needs at least one argument');
+        }
+        const mapper = args.pop();
+        assertFunction(mapper);
+        return seq.apply(null, args).map(function (results: any) {
+            return mapper.apply(null, results);
+        });
+    }
+
+    export function createLanguage(parsers: any) {
+        const language: any = {};
+        for (const key of Object.keys(parsers)) {
+            (function (key) {
+                language[key] = lazy(() => parsers[key](language));
+            })(key);
+        }
+        return language;
+    }
 
     export function seq<A>(a: MonadicParser<A>): MonadicParser<[A]>
     export function seq<A, B>(a: MonadicParser<A>, b: MonadicParser<B>): MonadicParser<[A, B]>
@@ -326,7 +337,7 @@ export namespace MonadicParser {
     export function regexp(re: RegExp, group = 0) {
         const anchored = anchoredRegexp(re);
         const expected = '' + re;
-        return new MonadicParser(function (input, i) {
+        return new MonadicParser((input, i) => {
             const match = anchored.exec(input.slice(i));
             if (match) {
                 if (0 <= group && group <= match.length) {
@@ -455,6 +466,15 @@ export namespace MonadicParser {
     export const crlf = string('\r\n');
     export const newline = alt(crlf, lf, cr).desc('newline');
     export const end = alt(newline, eof);
+
+    export function of<A>(value: A) {
+        return succeed(value);
+    }
+
+    export function regex(re: RegExp) {
+        return regexp(re);
+    }
+
 }
 
 function seqPick(idx: number, ...parsers: MonadicParser<any>[]): MonadicParser<any> {
@@ -550,4 +570,10 @@ function unsafeUnion(xs: string[], ys: string[]) {
 
 function isParser(obj: any): obj is MonadicParser<any> {
     return obj instanceof MonadicParser;
-}
\ No newline at end of file
+}
+
+function assertFunction(x: any) {
+    if (typeof x !== 'function') {
+        throw new Error('not a function: ' + x);
+    }
+}
diff --git a/src/mol-util/object.ts b/src/mol-util/object.ts
index d11c4bb9ef85675352f05aca5cdef5c26579021e..1cdbe8e9ec666d80b93bfb8279d582aeac819e63 100644
--- a/src/mol-util/object.ts
+++ b/src/mol-util/object.ts
@@ -8,7 +8,7 @@
 const hasOwnProperty = Object.prototype.hasOwnProperty;
 
 /** Assign to the object if a given property in update is undefined */
-export function assignIfUndefined<T>(to: Partial<T>, full: T): T {
+export function assignIfUndefined<T extends {}>(to: Partial<T>, full: T): T {
     for (const k of Object.keys(full)) {
         if (!hasOwnProperty.call(full, k)) continue;
 
@@ -20,7 +20,7 @@ export function assignIfUndefined<T>(to: Partial<T>, full: T): T {
 }
 
 /** Create new object if any property in "update" changes in "source". */
-export function shallowMerge2<T>(source: T, update: Partial<T>): T {
+export function shallowMerge2<T extends {}>(source: T, update: Partial<T>): T {
     // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
     let changed = false;
     for (const k of Object.keys(update)) {
@@ -36,7 +36,7 @@ export function shallowMerge2<T>(source: T, update: Partial<T>): T {
     return Object.assign({}, source, update);
 }
 
-export function shallowEqual<T>(a: T, b: T) {
+export function shallowEqual<T extends {}>(a: T, b: T) {
     if (!a) {
         if (!b) return true;
         return false;
@@ -52,11 +52,11 @@ export function shallowEqual<T>(a: T, b: T) {
     return true;
 }
 
-export function shallowMerge<T>(source: T, ...rest: (Partial<T> | undefined)[]): T {
+export function shallowMerge<T extends {}>(source: T, ...rest: (Partial<T> | undefined)[]): T {
     return shallowMergeArray(source, rest);
 }
 
-export function shallowMergeArray<T>(source: T, rest: (Partial<T> | undefined)[]): T {
+export function shallowMergeArray<T extends {}>(source: T, rest: (Partial<T> | undefined)[]): T {
     // Adapted from LiteMol (https://github.com/dsehnal/LiteMol)
     let ret: any = source;
 
diff --git a/src/mol-util/param-definition.ts b/src/mol-util/param-definition.ts
index 46840b54a6c17dcb13907257c6c9d726bb813594..a9697f5f454d63753f657bba63ff2ab32771eb3c 100644
--- a/src/mol-util/param-definition.ts
+++ b/src/mol-util/param-definition.ts
@@ -324,7 +324,7 @@ export namespace ParamDefinition {
         conditionForValue(v: T): keyof C
         conditionedValue(v: T, condition: keyof C): T,
     }
-    export function Conditioned<T, P extends Base<T>, C = { [k: string]: P }>(defaultValue: T, conditionParams: C, conditionForValue: (v: T) => keyof C, conditionedValue: (v: T, condition: keyof C) => T, info?: Info): Conditioned<T, P, C> {
+    export function Conditioned<T, P extends Base<T>, C extends {} = { [k: string]: P }>(defaultValue: T, conditionParams: C, conditionForValue: (v: T) => keyof C, conditionedValue: (v: T, condition: keyof C) => T, info?: Info): Conditioned<T, P, C> {
         const options = Object.keys(conditionParams).map(k => [k, k]) as [string, string][];
         return setInfo({ type: 'conditioned', select: Select<string>(conditionForValue(defaultValue) as string, options, info), defaultValue, conditionParams, conditionForValue, conditionedValue }, info);
     }
diff --git a/src/mol-util/single-async-queue.ts b/src/mol-util/single-async-queue.ts
new file mode 100644
index 0000000000000000000000000000000000000000..be62698d1af38e9c140324e9784b21530f051749
--- /dev/null
+++ b/src/mol-util/single-async-queue.ts
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Adam Midlik <midlik@gmail.com>
+ */
+
+
+/** Job queue that allows at most one running and one pending job.
+ * A newly enqueued job will cancel any other pending jobs. */
+export class SingleAsyncQueue {
+    private isRunning: boolean;
+    private queue: { id: number, func: () => any }[];
+    private counter: number;
+    private log: boolean;
+    constructor(log: boolean = false) {
+        this.isRunning = false;
+        this.queue = [];
+        this.counter = 0;
+        this.log = log;
+    }
+    enqueue(job: () => any) {
+        if (this.log) console.log('SingleAsyncQueue enqueue', this.counter);
+        this.queue[0] = { id: this.counter, func: job };
+        this.counter++;
+        this.run(); // do not await
+    }
+    private async run() {
+        if (this.isRunning) return;
+        const job = this.queue.pop();
+        if (!job) return;
+        this.isRunning = true;
+        try {
+            if (this.log) console.log('SingleAsyncQueue run', job.id);
+            await job.func();
+            if (this.log) console.log('SingleAsyncQueue complete', job.id);
+        } finally {
+            this.isRunning = false;
+            this.run();
+        }
+    }
+}
diff --git a/src/tests/browser/marching-cubes.ts b/src/tests/browser/marching-cubes.ts
index 8c501aa700eaf6213f255cb5cb71cf5780f1c49d..b9ef65cb60e415ff238cd5fe8724cebc13df73b5 100644
--- a/src/tests/browser/marching-cubes.ts
+++ b/src/tests/browser/marching-cubes.ts
@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2019-2021 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2019-2022 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -22,6 +22,7 @@ import { Representation } from '../../mol-repr/representation';
 import { computeMarchingCubesMesh } from '../../mol-geo/util/marching-cubes/algorithm';
 import { Mesh } from '../../mol-geo/geometry/mesh/mesh';
 import { ParamDefinition as PD } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -31,7 +32,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager), PD.merge(Canvas3DParams, PD.getDefaultValues(Canvas3DParams), {
     renderer: { backgroundColor: ColorNames.white },
     camera: { mode: 'orthographic' }
 }));
@@ -73,7 +76,7 @@ async function init() {
         console.timeEnd('gpu mc pyramid2');
 
         console.time('gpu mc vert2');
-        createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue, false, true, Vec3.create(0, 1, 2));
+        createIsosurfaceBuffers(webgl, activeVoxelsTex2, densityTextureData2.texture, compacted2, densityTextureData2.gridDim, densityTextureData2.gridTexDim, densityTextureData2.transform, isoValue, false, true, Vec3.create(0, 1, 2), true);
         webgl.waitForGpuCommandsCompleteSync();
         console.timeEnd('gpu mc vert2');
         console.timeEnd('gpu mc2');
@@ -96,7 +99,7 @@ async function init() {
     console.timeEnd('gpu mc pyramid');
 
     console.time('gpu mc vert');
-    const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue, false, true, Vec3.create(0, 1, 2));
+    const gv = createIsosurfaceBuffers(webgl, activeVoxelsTex, densityTextureData.texture, compacted, densityTextureData.gridDim, densityTextureData.gridTexDim, densityTextureData.transform, isoValue, false, true, Vec3.create(0, 1, 2), true);
     webgl.waitForGpuCommandsCompleteSync();
     console.timeEnd('gpu mc vert');
     console.timeEnd('gpu mc');
diff --git a/src/tests/browser/render-lines.ts b/src/tests/browser/render-lines.ts
index d4996f777d1ffcb2865e951c41935c296d7aa03d..137ca2a69f9d71023fd9d3f5d4ba3af469b6bcf6 100644
--- a/src/tests/browser/render-lines.ts
+++ b/src/tests/browser/render-lines.ts
@@ -15,6 +15,7 @@ import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -24,7 +25,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 
 function linesRepr() {
diff --git a/src/tests/browser/render-mesh.ts b/src/tests/browser/render-mesh.ts
index 12861bad246a92f054407259eef86686d45cfbde..e639b931e7dc826a5f7ab86808ed74cc8da1c8ce 100644
--- a/src/tests/browser/render-mesh.ts
+++ b/src/tests/browser/render-mesh.ts
@@ -17,6 +17,7 @@ import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { Torus } from '../../mol-geo/primitive/torus';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -26,7 +27,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 
 function meshRepr() {
diff --git a/src/tests/browser/render-shape.ts b/src/tests/browser/render-shape.ts
index cf83c1c9a4f8518ecf84416e804b07e594ee5552..184d273939372d42893fa6086f06b32792266cd0 100644
--- a/src/tests/browser/render-shape.ts
+++ b/src/tests/browser/render-shape.ts
@@ -19,6 +19,7 @@ import { Sphere } from '../../mol-geo/primitive/sphere';
 import { ColorNames } from '../../mol-util/color/names';
 import { Shape } from '../../mol-model/shape';
 import { ShapeRepresentation } from '../../mol-repr/shape/representation';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -28,6 +29,8 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
+const assetManager = new AssetManager();
+
 const info = document.createElement('div');
 info.style.position = 'absolute';
 info.style.fontFamily = 'sans-serif';
@@ -38,7 +41,7 @@ info.style.color = 'white';
 parent.appendChild(info);
 
 let prevReprLoci = Representation.Loci.Empty;
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 canvas3d.input.move.subscribe(({ x, y }) => {
     const pickingId = canvas3d.identify(x, y)?.id;
diff --git a/src/tests/browser/render-spheres.ts b/src/tests/browser/render-spheres.ts
index 439429fe35d6efec99e0ec225ab266b48e98b8a0..ed4e92ae278411bd11b155dbb3cc23fd38fd6b67 100644
--- a/src/tests/browser/render-spheres.ts
+++ b/src/tests/browser/render-spheres.ts
@@ -13,6 +13,7 @@ import { Color } from '../../mol-util/color';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Representation } from '../../mol-repr/representation';
 import { ParamDefinition } from '../../mol-util/param-definition';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -22,7 +23,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 
 function spheresRepr() {
diff --git a/src/tests/browser/render-structure.ts b/src/tests/browser/render-structure.ts
index 07e23ee4918d7a8698ceb01cc843ec62a4ceb614..634ccd8adeb7366d499ef77d0afffc6444961317 100644
--- a/src/tests/browser/render-structure.ts
+++ b/src/tests/browser/render-structure.ts
@@ -37,7 +37,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 
 const info = document.createElement('div');
@@ -123,7 +125,7 @@ function getMembraneOrientationRepr() {
 }
 
 async function init() {
-    const ctx = { runtime: SyncRuntimeContext, assetManager: new AssetManager() };
+    const ctx = { runtime: SyncRuntimeContext, assetManager };
 
     const cif = await downloadFromPdb('3pqr');
     const models = await getModels(cif);
diff --git a/src/tests/browser/render-text.ts b/src/tests/browser/render-text.ts
index c25a45fa195c9d1e3d8eaf1f0b32db0a0058bcbe..b1b0a33a09346f88270be7bf3138ed366b0e4ece 100644
--- a/src/tests/browser/render-text.ts
+++ b/src/tests/browser/render-text.ts
@@ -15,6 +15,7 @@ import { SpheresBuilder } from '../../mol-geo/geometry/spheres/spheres-builder';
 import { createRenderObject } from '../../mol-gl/render-object';
 import { Spheres } from '../../mol-geo/geometry/spheres/spheres';
 import { resizeCanvas } from '../../mol-canvas3d/util';
+import { AssetManager } from '../../mol-util/assets';
 
 const parent = document.getElementById('app')!;
 parent.style.width = '100%';
@@ -24,7 +25,9 @@ const canvas = document.createElement('canvas');
 parent.appendChild(canvas);
 resizeCanvas(canvas, parent);
 
-const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas));
+const assetManager = new AssetManager();
+
+const canvas3d = Canvas3D.create(Canvas3DContext.fromCanvas(canvas, assetManager));
 canvas3d.animate();
 
 function textRepr() {
diff --git a/webpack.config.common.js b/webpack.config.common.js
index 491eb8d551226831574049bcd850af99c1b939f7..380e1cc3e871daf21e71553e7b0efe452aacd3cc 100644
--- a/webpack.config.common.js
+++ b/webpack.config.common.js
@@ -30,7 +30,11 @@ const sharedConfig = {
                     { loader: 'css-loader', options: { sourceMap: false } },
                     { loader: 'sass-loader', options: { sourceMap: false } },
                 ]
-            }
+            },
+            {
+                test: /\.(jpg)$/i,
+                type: 'asset/resource',
+            },
         ]
     },
     plugins: [
@@ -76,7 +80,7 @@ function createEntry(src, outFolder, outFilename, isNode) {
 function createEntryPoint(name, dir, out, library) {
     return {
         entry: path.resolve(__dirname, `lib/${dir}/${name}.js`),
-        output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd' },
+        output: { filename: `${library || name}.js`, path: path.resolve(__dirname, `build/${out}`), library: library || out, libraryTarget: 'umd', assetModuleFilename: 'images/[hash][ext][query]', 'publicPath': '' },
         ...sharedConfig
     };
 }