Browse Source

Merge branch 'master' into mol-script

David Sehnal 6 years ago
parent
commit
8175430fe5
100 changed files with 7890 additions and 1274 deletions
  1. 25 3
      README.md
  2. 19 0
      data/mmcif-field-names.csv
  3. 18 0
      data/rcsb-graphql/codegen.js
  4. 8 0
      data/rcsb-graphql/codegen.json
  5. 1038 0
      examples/1crn.cif
  6. 422 271
      package-lock.json
  7. 31 26
      package.json
  8. 46 21
      src/apps/cif2bcif/converter.ts
  9. 174 3
      src/apps/cif2bcif/field-classifier.ts
  10. 4 4
      src/apps/domain-annotation-server/mapping.ts
  11. 0 37
      src/apps/domain-annotation-server/utils.ts
  12. 8 0
      src/apps/schema-generator/util/cif-dic.ts
  13. 143 71
      src/apps/structure-info/model.ts
  14. 25 0
      src/apps/viewer/index.tsx
  15. 3 0
      src/helpers.d.ts
  16. 7 1
      src/mol-app/context/context.ts
  17. 21 0
      src/mol-app/controller/visualization/sequence-view.ts
  18. 7 0
      src/mol-app/event/basic.ts
  19. 9 0
      src/mol-app/skin/components/sequence-view.scss
  20. 1 1
      src/mol-app/skin/layout/common.scss
  21. 2 1
      src/mol-app/skin/ui.scss
  22. 6 0
      src/mol-app/ui/controls/slider.tsx
  23. 15 3
      src/mol-app/ui/entity/tree.tsx
  24. 243 0
      src/mol-app/ui/transform/backbone.tsx
  25. 234 0
      src/mol-app/ui/transform/ball-and-stick.tsx
  26. 243 0
      src/mol-app/ui/transform/cartoon.tsx
  27. 234 0
      src/mol-app/ui/transform/distance-restraint.tsx
  28. 46 4
      src/mol-app/ui/transform/file-loader.tsx
  29. 15 0
      src/mol-app/ui/transform/list.tsx
  30. 33 2
      src/mol-app/ui/transform/spacefill.tsx
  31. 124 0
      src/mol-app/ui/transform/url-loader.tsx
  32. 6 0
      src/mol-app/ui/visualization/image-canvas.tsx
  33. 85 0
      src/mol-app/ui/visualization/sequence-view.tsx
  34. 23 5
      src/mol-app/ui/visualization/viewport.tsx
  35. 12 2
      src/mol-data/db/column.ts
  36. 13 0
      src/mol-data/db/table.ts
  37. 6 0
      src/mol-data/int/_spec/interval.spec.ts
  38. 25 0
      src/mol-data/int/_spec/ordered-set.spec.ts
  39. 2 2
      src/mol-data/int/_spec/segmentation.spec.ts
  40. 6 0
      src/mol-data/int/_spec/sorted-array.spec.ts
  41. 72 0
      src/mol-data/int/_spec/sorted-ranges.spec.ts
  42. 4 0
      src/mol-data/int/impl/interval.ts
  43. 31 1
      src/mol-data/int/impl/ordered-set.ts
  44. 18 19
      src/mol-data/int/impl/segmentation.ts
  45. 60 20
      src/mol-data/int/impl/sorted-array.ts
  46. 28 26
      src/mol-data/int/interval.ts
  47. 47 21
      src/mol-data/int/ordered-set.ts
  48. 14 10
      src/mol-data/int/segmentation.ts
  49. 38 33
      src/mol-data/int/sorted-array.ts
  50. 86 0
      src/mol-data/int/sorted-ranges.ts
  51. 1 0
      src/mol-data/util.ts
  52. 43 0
      src/mol-data/util/_spec/buckets.spec.ts
  53. 29 0
      src/mol-data/util/_spec/combination.spec.ts
  54. 33 0
      src/mol-data/util/_spec/interval-iterator.spec.ts
  55. 24 0
      src/mol-data/util/array.ts
  56. 104 0
      src/mol-data/util/buckets.ts
  57. 65 0
      src/mol-data/util/combination.ts
  58. 8 0
      src/mol-data/util/hash-functions.ts
  59. 45 0
      src/mol-data/util/interval-iterator.ts
  60. 45 77
      src/mol-geo/primitive/box.ts
  61. 7 7
      src/mol-geo/primitive/cylinder.ts
  62. 12 24
      src/mol-geo/primitive/icosahedron.ts
  63. 22 0
      src/mol-geo/primitive/octahedron.ts
  64. 36 0
      src/mol-geo/primitive/plane.ts
  65. 23 0
      src/mol-geo/primitive/polygon.ts
  66. 2 16
      src/mol-geo/primitive/polyhedron.ts
  67. 59 0
      src/mol-geo/primitive/primitive.ts
  68. 100 0
      src/mol-geo/primitive/prism.ts
  69. 60 0
      src/mol-geo/primitive/pyramid.ts
  70. 208 0
      src/mol-geo/primitive/sheet.ts
  71. 27 0
      src/mol-geo/primitive/sphere.ts
  72. 55 0
      src/mol-geo/primitive/star.ts
  73. 154 0
      src/mol-geo/primitive/tube.ts
  74. 122 0
      src/mol-geo/primitive/wedge.ts
  75. 19 5
      src/mol-geo/representation/index.ts
  76. 52 0
      src/mol-geo/representation/structure/backbone.ts
  77. 80 0
      src/mol-geo/representation/structure/ball-and-stick.ts
  78. 88 0
      src/mol-geo/representation/structure/cartoon.ts
  79. 55 0
      src/mol-geo/representation/structure/distance-restraint.ts
  80. 169 103
      src/mol-geo/representation/structure/index.ts
  81. 0 169
      src/mol-geo/representation/structure/point.ts
  82. 6 153
      src/mol-geo/representation/structure/spacefill.ts
  83. 0 133
      src/mol-geo/representation/structure/utils.ts
  84. 214 0
      src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts
  85. 165 0
      src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts
  86. 141 0
      src/mol-geo/representation/structure/visual/element-point.ts
  87. 116 0
      src/mol-geo/representation/structure/visual/element-sphere.ts
  88. 158 0
      src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts
  89. 179 0
      src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts
  90. 206 0
      src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts
  91. 152 0
      src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts
  92. 0 0
      src/mol-geo/representation/structure/visual/polymer-backbone-sphere.ts
  93. 184 0
      src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts
  94. 158 0
      src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts
  95. 177 0
      src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts
  96. 69 0
      src/mol-geo/representation/structure/visual/util/common.ts
  97. 103 0
      src/mol-geo/representation/structure/visual/util/element.ts
  98. 122 0
      src/mol-geo/representation/structure/visual/util/link.ts
  99. 69 0
      src/mol-geo/representation/structure/visual/util/polymer.ts
  100. 144 0
      src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts

+ 25 - 3
README.md

@@ -1,5 +1,6 @@
 [![License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](./LICENSE)
-[![Build Status](https://travis-ci.org/mol-star/mol-star-proto.svg?branch=master)](https://travis-ci.org/mol-star/mol-star-proto)
+[![Build Status](https://travis-ci.org/molstar/molstar-proto.svg?branch=master)](https://travis-ci.org/mol-star/mol-star-proto)
+[![Gitter](https://badges.gitter.im/molstar/Lobby.svg)](https://gitter.im/molstar/Lobby)
 
 # Mol*
 
@@ -51,19 +52,40 @@ This project builds on experience from previous solutions:
     npm run watch-extra
 
 ### Build/watch mol-viewer
-Build:
+**Build**
 
     npm run build
     npm run build-viewer
 
-Watch:
+**Watch**
 
     npm run watch
     npm run watch-extra
     npm run watch-viewer
 
+**Run**
+
+If not installed previously:
+
+    npm install -g http-server
+
+...or a similar solution.
+
+From the root of the project:
+
+    http-server -p PORT-NUMBER
+
+and navigate to `build/viewer`
+
+
+
+
 ## Contributing
 Just open an issue or make a pull request. All contributions are welcome.
 
 ## Roadmap
 Continually develop this prototype project. As individual modules become stable, make them into standalone libraries.
+
+## Funding
+Funding sources include but are not limted to:
+* [RCSB PDB](https://www.rcsb.org) funding by a grant [DBI-1338415; PI: SK Burley] from the NSF, the NIH, and the US DoE

+ 19 - 0
data/mmcif-field-names.csv

@@ -62,6 +62,15 @@ entity.pdbx_mutation
 entity.pdbx_fragment
 entity.pdbx_ec
 
+entity_poly.entity_id
+entity_poly.type
+entity_poly.nstd_linkage
+entity_poly.nstd_monomer
+entity_poly.pdbx_seq_one_letter_code  
+entity_poly.pdbx_seq_one_letter_code_can 
+entity_poly.pdbx_strand_id
+entity_poly.pdbx_target_identifier
+
 entity_poly_seq.entity_id
 entity_poly_seq.num
 entity_poly_seq.mon_id
@@ -144,6 +153,12 @@ struct_keywords.entry_id
 struct_keywords.pdbx_keywords
 struct_keywords.text
 
+struct_ncs_oper.id
+struct_ncs_oper.code
+struct_ncs_oper.matrix
+struct_ncs_oper.vector
+struct_ncs_oper.details
+
 struct_sheet_range.sheet_id
 struct_sheet_range.id
 struct_sheet_range.beg_label_comp_id
@@ -359,10 +374,12 @@ ihm_cross_link_restraint.group_id
 ihm_cross_link_restraint.entity_id_1
 ihm_cross_link_restraint.asym_id_1
 ihm_cross_link_restraint.seq_id_1
+ihm_cross_link_restraint.atom_id_1
 ihm_cross_link_restraint.comp_id_1
 ihm_cross_link_restraint.entity_id_2
 ihm_cross_link_restraint.asym_id_2
 ihm_cross_link_restraint.seq_id_2
+ihm_cross_link_restraint.atom_id_2
 ihm_cross_link_restraint.comp_id_2
 ihm_cross_link_restraint.restraint_type
 ihm_cross_link_restraint.conditional_crosslink_flag
@@ -428,6 +445,7 @@ ihm_3dem_restraint.model_id
 ihm_3dem_restraint.cross_correlation_coefficient
 
 ihm_predicted_contact_restraint.id
+ihm_predicted_contact_restraint.group_id
 ihm_predicted_contact_restraint.entity_id_1
 ihm_predicted_contact_restraint.asym_id_1
 ihm_predicted_contact_restraint.seq_id_1
@@ -439,6 +457,7 @@ ihm_predicted_contact_restraint.seq_id_2
 ihm_predicted_contact_restraint.comp_id_2
 ihm_predicted_contact_restraint.atom_id_2
 ihm_predicted_contact_restraint.restraint_type
+ihm_predicted_contact_restraint.distance_lower_limit
 ihm_predicted_contact_restraint.distance_upper_limit
 ihm_predicted_contact_restraint.probability
 ihm_predicted_contact_restraint.model_granularity

+ 18 - 0
data/rcsb-graphql/codegen.js

@@ -0,0 +1,18 @@
+const { generate } = require('graphql-code-generator')
+const path = require('path')
+
+const basePath = path.join(__dirname, '..', '..', 'src', 'servers', 'model', 'properties', 'rcsb', 'graphql')
+
+generate({
+    args: [
+        path.join(basePath, 'symmetry.gql.ts')
+    ],
+    schema: 'http://rest-experimental.rcsb.org/graphql',
+    template: 'typescript',
+    out: path.join(basePath),
+    skipSchema: true,
+    overwrite: true,
+    config: path.join(__dirname, 'codegen.json')
+}, true).then(
+    console.log('done')
+).catch(e => console.error(e))

+ 8 - 0
data/rcsb-graphql/codegen.json

@@ -0,0 +1,8 @@
+{
+    "flattenTypes": false,
+    "generatorConfig": {
+        "printTime": true,
+        "immutableTypes": true,
+        "resolvers": false
+    }
+}

+ 1038 - 0
examples/1crn.cif

@@ -0,0 +1,1038 @@
+data_1CRN
+# 
+_entry.id   1CRN 
+# 
+_audit_conform.dict_name       mmcif_pdbx.dic 
+_audit_conform.dict_version    4.024 
+_audit_conform.dict_location   http://mmcif.pdb.org/dictionaries/ascii/mmcif_pdbx.dic 
+# 
+_database_2.database_id     PDB 
+_database_2.database_code   1CRN 
+# 
+loop_
+_database_PDB_rev.num 
+_database_PDB_rev.date 
+_database_PDB_rev.date_original 
+_database_PDB_rev.status 
+_database_PDB_rev.replaces 
+_database_PDB_rev.mod_type 
+1 1981-07-28 1981-04-30 ? 1CRN 0 
+2 1981-12-03 ?          ? 1CRN 1 
+3 1983-09-30 ?          ? 1CRN 1 
+4 1985-03-04 ?          ? 1CRN 1 
+5 1987-04-16 ?          ? 1CRN 1 
+6 2009-02-24 ?          ? 1CRN 1 
+7 2012-07-11 ?          ? 1CRN 1 
+# 
+loop_
+_database_PDB_rev_record.rev_num 
+_database_PDB_rev_record.type 
+_database_PDB_rev_record.details 
+2 SHEET  ? 
+3 REVDAT ? 
+4 REMARK ? 
+5 HEADER ? 
+6 VERSN  ? 
+7 SCALE1 ? 
+7 VERSN  ? 
+7 HEADER ? 
+# 
+_pdbx_database_status.status_code                    REL 
+_pdbx_database_status.entry_id                       1CRN 
+_pdbx_database_status.deposit_site                   ? 
+_pdbx_database_status.process_site                   ? 
+_pdbx_database_status.SG_entry                       . 
+_pdbx_database_status.status_code_sf                 ? 
+_pdbx_database_status.status_code_mr                 ? 
+_pdbx_database_status.status_code_cs                 ? 
+_pdbx_database_status.methods_development_category   ? 
+# 
+loop_
+_audit_author.name 
+_audit_author.pdbx_ordinal 
+'Hendrickson, W.A.' 1 
+'Teeter, M.M.'      2 
+# 
+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 'Water structure of a hydrophobic protein at atomic resolution: Pentagon rings of water molecules in crystals of crambin.' 
+Proc.Natl.Acad.Sci.Usa 81  6014 6018 1984 PNASA6 US 0027-8424 0040 ? 16593516 10.1073/pnas.81.19.6014 
+1       'Structure of the Hydrophobic Protein Crambin Determined Directly from the Anomalous Scattering of Sulphur'                
+Nature                 290 107  ?    1981 NATUAS UK 0028-0836 0006 ? ?        ?                       
+2       'Highly Ordered Crystals of the Plant Seed Protein Crambin'                                                                
+J.Mol.Biol.            127 219  ?    1979 JMOBAK UK 0022-2836 0070 ? ?        ?                       
+# 
+loop_
+_citation_author.citation_id 
+_citation_author.name 
+_citation_author.ordinal 
+primary 'Teeter, M.M.'      1 
+1       'Hendrickson, W.A.' 2 
+1       'Teeter, M.M.'      3 
+2       'Teeter, M.M.'      4 
+2       'Hendrickson, W.A.' 5 
+# 
+_cell.entry_id           1CRN 
+_cell.length_a           40.960 
+_cell.length_b           18.650 
+_cell.length_c           22.520 
+_cell.angle_alpha        90.00 
+_cell.angle_beta         90.77 
+_cell.angle_gamma        90.00 
+_cell.Z_PDB              2 
+_cell.pdbx_unique_axis   ? 
+_cell.length_a_esd       ? 
+_cell.length_b_esd       ? 
+_cell.length_c_esd       ? 
+_cell.angle_alpha_esd    ? 
+_cell.angle_beta_esd     ? 
+_cell.angle_gamma_esd    ? 
+# 
+_symmetry.entry_id                         1CRN 
+_symmetry.space_group_name_H-M             'P 1 21 1' 
+_symmetry.pdbx_full_space_group_name_H-M   ? 
+_symmetry.cell_setting                     ? 
+_symmetry.Int_Tables_number                ? 
+_symmetry.space_group_name_Hall            ? 
+# 
+_entity.id                         1 
+_entity.type                       polymer 
+_entity.src_method                 man 
+_entity.pdbx_description           CRAMBIN 
+_entity.formula_weight             4738.464 
+_entity.pdbx_number_of_molecules   1 
+_entity.details                    ? 
+# 
+_entity_poly.entity_id                      1 
+_entity_poly.type                           'polypeptide(L)' 
+_entity_poly.nstd_linkage                   no 
+_entity_poly.nstd_monomer                   no 
+_entity_poly.pdbx_seq_one_letter_code       TTCCPSIVARSNFNVCRLPGTPEAICATYTGCIIIPGATCPGDYAN 
+_entity_poly.pdbx_seq_one_letter_code_can   TTCCPSIVARSNFNVCRLPGTPEAICATYTGCIIIPGATCPGDYAN 
+_entity_poly.pdbx_strand_id                 A 
+# 
+loop_
+_entity_poly_seq.entity_id 
+_entity_poly_seq.num 
+_entity_poly_seq.mon_id 
+_entity_poly_seq.hetero 
+1 1  THR n 
+1 2  THR n 
+1 3  CYS n 
+1 4  CYS n 
+1 5  PRO n 
+1 6  SER n 
+1 7  ILE n 
+1 8  VAL n 
+1 9  ALA n 
+1 10 ARG n 
+1 11 SER n 
+1 12 ASN n 
+1 13 PHE n 
+1 14 ASN n 
+1 15 VAL n 
+1 16 CYS n 
+1 17 ARG n 
+1 18 LEU n 
+1 19 PRO n 
+1 20 GLY n 
+1 21 THR n 
+1 22 PRO n 
+1 23 GLU n 
+1 24 ALA n 
+1 25 ILE n 
+1 26 CYS n 
+1 27 ALA n 
+1 28 THR n 
+1 29 TYR n 
+1 30 THR n 
+1 31 GLY n 
+1 32 CYS n 
+1 33 ILE n 
+1 34 ILE n 
+1 35 ILE n 
+1 36 PRO n 
+1 37 GLY n 
+1 38 ALA n 
+1 39 THR n 
+1 40 CYS n 
+1 41 PRO n 
+1 42 GLY n 
+1 43 ASP n 
+1 44 TYR n 
+1 45 ALA n 
+1 46 ASN n 
+# 
+_entity_src_gen.entity_id                          1 
+_entity_src_gen.gene_src_common_name               ? 
+_entity_src_gen.gene_src_genus                     Crambe 
+_entity_src_gen.pdbx_gene_src_gene                 ? 
+_entity_src_gen.gene_src_species                   'Crambe hispanica' 
+_entity_src_gen.gene_src_strain                    'subsp. abyssinica' 
+_entity_src_gen.gene_src_tissue                    ? 
+_entity_src_gen.gene_src_tissue_fraction           ? 
+_entity_src_gen.gene_src_details                   ? 
+_entity_src_gen.pdbx_gene_src_fragment             ? 
+_entity_src_gen.pdbx_gene_src_scientific_name      'Crambe hispanica subsp. abyssinica' 
+_entity_src_gen.pdbx_gene_src_ncbi_taxonomy_id     3721 
+_entity_src_gen.pdbx_gene_src_variant              ? 
+_entity_src_gen.pdbx_gene_src_cell_line            ? 
+_entity_src_gen.pdbx_gene_src_atcc                 ? 
+_entity_src_gen.pdbx_gene_src_organ                ? 
+_entity_src_gen.pdbx_gene_src_organelle            ? 
+_entity_src_gen.pdbx_gene_src_cell                 ? 
+_entity_src_gen.pdbx_gene_src_cellular_location    ? 
+_entity_src_gen.host_org_common_name               ? 
+_entity_src_gen.pdbx_host_org_scientific_name      ? 
+_entity_src_gen.pdbx_host_org_ncbi_taxonomy_id     ? 
+_entity_src_gen.host_org_genus                     ? 
+_entity_src_gen.pdbx_host_org_gene                 ? 
+_entity_src_gen.pdbx_host_org_organ                ? 
+_entity_src_gen.host_org_species                   ? 
+_entity_src_gen.pdbx_host_org_tissue               ? 
+_entity_src_gen.pdbx_host_org_tissue_fraction      ? 
+_entity_src_gen.pdbx_host_org_strain               ? 
+_entity_src_gen.pdbx_host_org_variant              ? 
+_entity_src_gen.pdbx_host_org_cell_line            ? 
+_entity_src_gen.pdbx_host_org_atcc                 ? 
+_entity_src_gen.pdbx_host_org_culture_collection   ? 
+_entity_src_gen.pdbx_host_org_cell                 ? 
+_entity_src_gen.pdbx_host_org_organelle            ? 
+_entity_src_gen.pdbx_host_org_cellular_location    ? 
+_entity_src_gen.pdbx_host_org_vector_type          ? 
+_entity_src_gen.pdbx_host_org_vector               ? 
+_entity_src_gen.plasmid_name                       ? 
+_entity_src_gen.plasmid_details                    ? 
+_entity_src_gen.pdbx_description                   ? 
+# 
+_struct_ref.id                         1 
+_struct_ref.db_name                    UNP 
+_struct_ref.db_code                    CRAM_CRAAB 
+_struct_ref.entity_id                  1 
+_struct_ref.pdbx_db_accession          P01542 
+_struct_ref.pdbx_align_begin           1 
+_struct_ref.pdbx_seq_one_letter_code   TTCCPSIVARSNFNVCRLPGTPEAICATYTGCIIIPGATCPGDYAN 
+_struct_ref.biol_id                    . 
+# 
+_struct_ref_seq.align_id                      1 
+_struct_ref_seq.ref_id                        1 
+_struct_ref_seq.pdbx_PDB_id_code              1CRN 
+_struct_ref_seq.pdbx_strand_id                A 
+_struct_ref_seq.seq_align_beg                 1 
+_struct_ref_seq.pdbx_seq_align_beg_ins_code   ? 
+_struct_ref_seq.seq_align_end                 46 
+_struct_ref_seq.pdbx_seq_align_end_ins_code   ? 
+_struct_ref_seq.pdbx_db_accession             P01542 
+_struct_ref_seq.db_align_beg                  1 
+_struct_ref_seq.pdbx_db_align_beg_ins_code    ? 
+_struct_ref_seq.db_align_end                  46 
+_struct_ref_seq.pdbx_db_align_end_ins_code    ? 
+_struct_ref_seq.pdbx_auth_seq_align_beg       1 
+_struct_ref_seq.pdbx_auth_seq_align_end       46 
+# 
+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 
+THR 'L-peptide linking' y THREONINE       ? 'C4 H9 N O3'     119.120 
+CYS 'L-peptide linking' y CYSTEINE        ? 'C3 H7 N O2 S'   121.154 
+PRO 'L-peptide linking' y PROLINE         ? 'C5 H9 N O2'     115.132 
+SER 'L-peptide linking' y SERINE          ? 'C3 H7 N O3'     105.093 
+ILE 'L-peptide linking' y ISOLEUCINE      ? 'C6 H13 N O2'    131.174 
+VAL 'L-peptide linking' y VALINE          ? 'C5 H11 N O2'    117.147 
+ALA 'L-peptide linking' y ALANINE         ? 'C3 H7 N O2'     89.094  
+ARG 'L-peptide linking' y ARGININE        ? 'C6 H15 N4 O2 1' 175.210 
+ASN 'L-peptide linking' y ASPARAGINE      ? 'C4 H8 N2 O3'    132.119 
+PHE 'L-peptide linking' y PHENYLALANINE   ? 'C9 H11 N O2'    165.191 
+LEU 'L-peptide linking' y LEUCINE         ? 'C6 H13 N O2'    131.174 
+GLY 'PEPTIDE LINKING'   y GLYCINE         ? 'C2 H5 N O2'     75.067  
+GLU 'L-peptide linking' y 'GLUTAMIC ACID' ? 'C5 H9 N O4'     147.130 
+TYR 'L-peptide linking' y TYROSINE        ? 'C9 H11 N O3'    181.191 
+ASP 'L-peptide linking' y 'ASPARTIC ACID' ? 'C4 H7 N O4'     133.104 
+# 
+_exptl.entry_id          1CRN 
+_exptl.method            'X-RAY DIFFRACTION' 
+_exptl.crystals_number   ? 
+# 
+_exptl_crystal.id                    1 
+_exptl_crystal.density_meas          ? 
+_exptl_crystal.density_Matthews      1.81 
+_exptl_crystal.density_percent_sol   32.16 
+_exptl_crystal.description           ? 
+_exptl_crystal.F_000                 ? 
+_exptl_crystal.preparation           ? 
+# 
+_diffrn.id                     1 
+_diffrn.ambient_temp           ? 
+_diffrn.ambient_temp_details   ? 
+_diffrn.crystal_id             1 
+# 
+_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 
+# 
+_computing.entry_id                           1CRN 
+_computing.pdbx_data_reduction_ii             ? 
+_computing.pdbx_data_reduction_ds             ? 
+_computing.data_collection                    ? 
+_computing.structure_solution                 ? 
+_computing.structure_refinement               PROLSQ 
+_computing.pdbx_structure_refinement_method   ? 
+# 
+_refine.entry_id                               1CRN 
+_refine.ls_number_reflns_obs                   ? 
+_refine.ls_number_reflns_all                   ? 
+_refine.pdbx_ls_sigma_I                        ? 
+_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                           ? 
+_refine.ls_d_res_high                          1.5 
+_refine.ls_percent_reflns_obs                  ? 
+_refine.ls_R_factor_obs                        ? 
+_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.ls_redundancy_reflns_obs               ? 
+_refine.pdbx_overall_phase_error               ? 
+_refine.B_iso_min                              ? 
+_refine.B_iso_max                              ? 
+_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.overall_SU_R_Cruickshank_DPI           ? 
+_refine.overall_SU_R_free                      ? 
+_refine.ls_wR_factor_R_free                    ? 
+_refine.ls_wR_factor_R_work                    ? 
+_refine.overall_FOM_free_R_set                 ? 
+_refine.overall_FOM_work_R_set                 ? 
+# 
+_refine_hist.pdbx_refine_id                   'X-RAY DIFFRACTION' 
+_refine_hist.cycle_id                         LAST 
+_refine_hist.pdbx_number_atoms_protein        327 
+_refine_hist.pdbx_number_atoms_nucleic_acid   0 
+_refine_hist.pdbx_number_atoms_ligand         0 
+_refine_hist.number_atoms_solvent             0 
+_refine_hist.number_atoms_total               327 
+_refine_hist.d_res_high                       1.5 
+_refine_hist.d_res_low                        . 
+# 
+_struct.entry_id                  1CRN 
+_struct.title                     
+'WATER STRUCTURE OF A HYDROPHOBIC PROTEIN AT ATOMIC RESOLUTION. PENTAGON RINGS OF WATER MOLECULES IN CRYSTALS OF CRAMBIN' 
+_struct.pdbx_descriptor           CRAMBIN 
+_struct.pdbx_model_details        ? 
+_struct.pdbx_CASP_flag            ? 
+_struct.pdbx_model_type_details   ? 
+# 
+_struct_keywords.entry_id        1CRN 
+_struct_keywords.pdbx_keywords   'PLANT PROTEIN' 
+_struct_keywords.text            'PLANT SEED PROTEIN, PLANT PROTEIN' 
+# 
+_struct_asym.id                            A 
+_struct_asym.pdbx_blank_PDB_chainid_flag   N 
+_struct_asym.pdbx_modified                 N 
+_struct_asym.entity_id                     1 
+_struct_asym.details                       ? 
+# 
+_struct_biol.id        1 
+_struct_biol.details   ? 
+# 
+loop_
+_struct_conf.conf_type_id 
+_struct_conf.id 
+_struct_conf.pdbx_PDB_helix_id 
+_struct_conf.beg_label_comp_id 
+_struct_conf.beg_label_asym_id 
+_struct_conf.beg_label_seq_id 
+_struct_conf.pdbx_beg_PDB_ins_code 
+_struct_conf.end_label_comp_id 
+_struct_conf.end_label_asym_id 
+_struct_conf.end_label_seq_id 
+_struct_conf.pdbx_end_PDB_ins_code 
+_struct_conf.beg_auth_comp_id 
+_struct_conf.beg_auth_asym_id 
+_struct_conf.beg_auth_seq_id 
+_struct_conf.end_auth_comp_id 
+_struct_conf.end_auth_asym_id 
+_struct_conf.end_auth_seq_id 
+_struct_conf.pdbx_PDB_helix_class 
+_struct_conf.details 
+_struct_conf.pdbx_PDB_helix_length 
+HELX_P HELX_P1 H1 ILE A 7  ? PRO A 19 ? ILE A 7  PRO A 19 1 '3/10 CONFORMATION RES 17,19' 13 
+HELX_P HELX_P2 H2 GLU A 23 ? THR A 30 ? GLU A 23 THR A 30 1 'DISTORTED 3/10 AT RES 30'    8  
+TURN_P TURN_P1 T1 PRO A 41 ? TYR A 44 ? PRO A 41 TYR A 44 ? ?                             ?  
+# 
+loop_
+_struct_conf_type.id 
+_struct_conf_type.criteria 
+_struct_conf_type.reference 
+HELX_P ? ? 
+TURN_P ? ? 
+# 
+loop_
+_struct_conn.id 
+_struct_conn.conn_type_id 
+_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 
+disulf1 disulf ? A CYS 3  SG ? ? ? 1_555 A CYS 40 SG ? ? A CYS 3  A CYS 40 1_555 ? ? ? ? ? ? ? 2.004 ? 
+disulf2 disulf ? A CYS 4  SG ? ? ? 1_555 A CYS 32 SG ? ? A CYS 4  A CYS 32 1_555 ? ? ? ? ? ? ? 2.035 ? 
+disulf3 disulf ? A CYS 16 SG ? ? ? 1_555 A CYS 26 SG ? ? A CYS 16 A CYS 26 1_555 ? ? ? ? ? ? ? 2.051 ? 
+# 
+_struct_conn_type.id          disulf 
+_struct_conn_type.criteria    ? 
+_struct_conn_type.reference   ? 
+# 
+_struct_sheet.id               S1 
+_struct_sheet.type             ? 
+_struct_sheet.number_strands   2 
+_struct_sheet.details          ? 
+# 
+_struct_sheet_order.sheet_id     S1 
+_struct_sheet_order.range_id_1   1 
+_struct_sheet_order.range_id_2   2 
+_struct_sheet_order.offset       ? 
+_struct_sheet_order.sense        anti-parallel 
+# 
+loop_
+_struct_sheet_range.sheet_id 
+_struct_sheet_range.id 
+_struct_sheet_range.beg_label_comp_id 
+_struct_sheet_range.beg_label_asym_id 
+_struct_sheet_range.beg_label_seq_id 
+_struct_sheet_range.pdbx_beg_PDB_ins_code 
+_struct_sheet_range.end_label_comp_id 
+_struct_sheet_range.end_label_asym_id 
+_struct_sheet_range.end_label_seq_id 
+_struct_sheet_range.pdbx_end_PDB_ins_code 
+_struct_sheet_range.symmetry 
+_struct_sheet_range.beg_auth_comp_id 
+_struct_sheet_range.beg_auth_asym_id 
+_struct_sheet_range.beg_auth_seq_id 
+_struct_sheet_range.end_auth_comp_id 
+_struct_sheet_range.end_auth_asym_id 
+_struct_sheet_range.end_auth_seq_id 
+S1 1 THR A 1  ? CYS A 4  ? ? THR A 1  CYS A 4  
+S1 2 CYS A 32 ? ILE A 35 ? ? CYS A 32 ILE A 35 
+# 
+_database_PDB_matrix.entry_id          1CRN 
+_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                    1CRN 
+_atom_sites.Cartn_transform_axes        ? 
+_atom_sites.fract_transf_matrix[1][1]   0.024414 
+_atom_sites.fract_transf_matrix[1][2]   0.000000 
+_atom_sites.fract_transf_matrix[1][3]   0.000328 
+_atom_sites.fract_transf_matrix[2][1]   0.000000 
+_atom_sites.fract_transf_matrix[2][2]   0.053619 
+_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.044409 
+_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 
+N 
+C 
+O 
+S 
+# 
+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.Cartn_x_esd 
+_atom_site.Cartn_y_esd 
+_atom_site.Cartn_z_esd 
+_atom_site.occupancy_esd 
+_atom_site.B_iso_or_equiv_esd 
+_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   N N   . THR A 1 1  ? 17.047 14.099 3.625  1.00 13.79 ? ? ? ? ? ? 1  THR A N   1 
+ATOM 2   C CA  . THR A 1 1  ? 16.967 12.784 4.338  1.00 10.80 ? ? ? ? ? ? 1  THR A CA  1 
+ATOM 3   C C   . THR A 1 1  ? 15.685 12.755 5.133  1.00 9.19  ? ? ? ? ? ? 1  THR A C   1 
+ATOM 4   O O   . THR A 1 1  ? 15.268 13.825 5.594  1.00 9.85  ? ? ? ? ? ? 1  THR A O   1 
+ATOM 5   C CB  . THR A 1 1  ? 18.170 12.703 5.337  1.00 13.02 ? ? ? ? ? ? 1  THR A CB  1 
+ATOM 6   O OG1 . THR A 1 1  ? 19.334 12.829 4.463  1.00 15.06 ? ? ? ? ? ? 1  THR A OG1 1 
+ATOM 7   C CG2 . THR A 1 1  ? 18.150 11.546 6.304  1.00 14.23 ? ? ? ? ? ? 1  THR A CG2 1 
+ATOM 8   N N   . THR A 1 2  ? 15.115 11.555 5.265  1.00 7.81  ? ? ? ? ? ? 2  THR A N   1 
+ATOM 9   C CA  . THR A 1 2  ? 13.856 11.469 6.066  1.00 8.31  ? ? ? ? ? ? 2  THR A CA  1 
+ATOM 10  C C   . THR A 1 2  ? 14.164 10.785 7.379  1.00 5.80  ? ? ? ? ? ? 2  THR A C   1 
+ATOM 11  O O   . THR A 1 2  ? 14.993 9.862  7.443  1.00 6.94  ? ? ? ? ? ? 2  THR A O   1 
+ATOM 12  C CB  . THR A 1 2  ? 12.732 10.711 5.261  1.00 10.32 ? ? ? ? ? ? 2  THR A CB  1 
+ATOM 13  O OG1 . THR A 1 2  ? 13.308 9.439  4.926  1.00 12.81 ? ? ? ? ? ? 2  THR A OG1 1 
+ATOM 14  C CG2 . THR A 1 2  ? 12.484 11.442 3.895  1.00 11.90 ? ? ? ? ? ? 2  THR A CG2 1 
+ATOM 15  N N   . CYS A 1 3  ? 13.488 11.241 8.417  1.00 5.24  ? ? ? ? ? ? 3  CYS A N   1 
+ATOM 16  C CA  . CYS A 1 3  ? 13.660 10.707 9.787  1.00 5.39  ? ? ? ? ? ? 3  CYS A CA  1 
+ATOM 17  C C   . CYS A 1 3  ? 12.269 10.431 10.323 1.00 4.45  ? ? ? ? ? ? 3  CYS A C   1 
+ATOM 18  O O   . CYS A 1 3  ? 11.393 11.308 10.185 1.00 6.54  ? ? ? ? ? ? 3  CYS A O   1 
+ATOM 19  C CB  . CYS A 1 3  ? 14.368 11.748 10.691 1.00 5.99  ? ? ? ? ? ? 3  CYS A CB  1 
+ATOM 20  S SG  . CYS A 1 3  ? 15.885 12.426 10.016 1.00 7.01  ? ? ? ? ? ? 3  CYS A SG  1 
+ATOM 21  N N   . CYS A 1 4  ? 12.019 9.272  10.928 1.00 3.90  ? ? ? ? ? ? 4  CYS A N   1 
+ATOM 22  C CA  . CYS A 1 4  ? 10.646 8.991  11.408 1.00 4.24  ? ? ? ? ? ? 4  CYS A CA  1 
+ATOM 23  C C   . CYS A 1 4  ? 10.654 8.793  12.919 1.00 3.72  ? ? ? ? ? ? 4  CYS A C   1 
+ATOM 24  O O   . CYS A 1 4  ? 11.659 8.296  13.491 1.00 5.30  ? ? ? ? ? ? 4  CYS A O   1 
+ATOM 25  C CB  . CYS A 1 4  ? 10.057 7.752  10.682 1.00 4.41  ? ? ? ? ? ? 4  CYS A CB  1 
+ATOM 26  S SG  . CYS A 1 4  ? 9.837  8.018  8.904  1.00 4.72  ? ? ? ? ? ? 4  CYS A SG  1 
+ATOM 27  N N   . PRO A 1 5  ? 9.561  9.108  13.563 1.00 3.96  ? ? ? ? ? ? 5  PRO A N   1 
+ATOM 28  C CA  . PRO A 1 5  ? 9.448  9.034  15.012 1.00 4.25  ? ? ? ? ? ? 5  PRO A CA  1 
+ATOM 29  C C   . PRO A 1 5  ? 9.288  7.670  15.606 1.00 4.96  ? ? ? ? ? ? 5  PRO A C   1 
+ATOM 30  O O   . PRO A 1 5  ? 9.490  7.519  16.819 1.00 7.44  ? ? ? ? ? ? 5  PRO A O   1 
+ATOM 31  C CB  . PRO A 1 5  ? 8.230  9.957  15.345 1.00 5.11  ? ? ? ? ? ? 5  PRO A CB  1 
+ATOM 32  C CG  . PRO A 1 5  ? 7.338  9.786  14.114 1.00 5.24  ? ? ? ? ? ? 5  PRO A CG  1 
+ATOM 33  C CD  . PRO A 1 5  ? 8.366  9.804  12.958 1.00 5.20  ? ? ? ? ? ? 5  PRO A CD  1 
+ATOM 34  N N   . SER A 1 6  ? 8.875  6.686  14.796 1.00 4.83  ? ? ? ? ? ? 6  SER A N   1 
+ATOM 35  C CA  . SER A 1 6  ? 8.673  5.314  15.279 1.00 4.45  ? ? ? ? ? ? 6  SER A CA  1 
+ATOM 36  C C   . SER A 1 6  ? 8.753  4.376  14.083 1.00 4.99  ? ? ? ? ? ? 6  SER A C   1 
+ATOM 37  O O   . SER A 1 6  ? 8.726  4.858  12.923 1.00 4.61  ? ? ? ? ? ? 6  SER A O   1 
+ATOM 38  C CB  . SER A 1 6  ? 7.340  5.121  15.996 1.00 5.05  ? ? ? ? ? ? 6  SER A CB  1 
+ATOM 39  O OG  . SER A 1 6  ? 6.274  5.220  15.031 1.00 6.39  ? ? ? ? ? ? 6  SER A OG  1 
+ATOM 40  N N   . ILE A 1 7  ? 8.881  3.075  14.358 1.00 4.94  ? ? ? ? ? ? 7  ILE A N   1 
+ATOM 41  C CA  . ILE A 1 7  ? 8.912  2.083  13.258 1.00 6.33  ? ? ? ? ? ? 7  ILE A CA  1 
+ATOM 42  C C   . ILE A 1 7  ? 7.581  2.090  12.506 1.00 5.32  ? ? ? ? ? ? 7  ILE A C   1 
+ATOM 43  O O   . ILE A 1 7  ? 7.670  2.031  11.245 1.00 6.85  ? ? ? ? ? ? 7  ILE A O   1 
+ATOM 44  C CB  . ILE A 1 7  ? 9.207  0.677  13.924 1.00 8.43  ? ? ? ? ? ? 7  ILE A CB  1 
+ATOM 45  C CG1 . ILE A 1 7  ? 10.714 0.702  14.312 1.00 9.78  ? ? ? ? ? ? 7  ILE A CG1 1 
+ATOM 46  C CG2 . ILE A 1 7  ? 8.811  -0.477 12.969 1.00 11.70 ? ? ? ? ? ? 7  ILE A CG2 1 
+ATOM 47  C CD1 . ILE A 1 7  ? 11.185 -0.516 15.142 1.00 9.92  ? ? ? ? ? ? 7  ILE A CD1 1 
+ATOM 48  N N   . VAL A 1 8  ? 6.458  2.162  13.159 1.00 5.02  ? ? ? ? ? ? 8  VAL A N   1 
+ATOM 49  C CA  . VAL A 1 8  ? 5.145  2.209  12.453 1.00 6.93  ? ? ? ? ? ? 8  VAL A CA  1 
+ATOM 50  C C   . VAL A 1 8  ? 5.115  3.379  11.461 1.00 5.39  ? ? ? ? ? ? 8  VAL A C   1 
+ATOM 51  O O   . VAL A 1 8  ? 4.664  3.268  10.343 1.00 6.30  ? ? ? ? ? ? 8  VAL A O   1 
+ATOM 52  C CB  . VAL A 1 8  ? 3.995  2.354  13.478 1.00 9.64  ? ? ? ? ? ? 8  VAL A CB  1 
+ATOM 53  C CG1 . VAL A 1 8  ? 2.716  2.891  12.869 1.00 13.85 ? ? ? ? ? ? 8  VAL A CG1 1 
+ATOM 54  C CG2 . VAL A 1 8  ? 3.758  1.032  14.208 1.00 11.97 ? ? ? ? ? ? 8  VAL A CG2 1 
+ATOM 55  N N   . ALA A 1 9  ? 5.606  4.546  11.941 1.00 3.73  ? ? ? ? ? ? 9  ALA A N   1 
+ATOM 56  C CA  . ALA A 1 9  ? 5.598  5.767  11.082 1.00 3.56  ? ? ? ? ? ? 9  ALA A CA  1 
+ATOM 57  C C   . ALA A 1 9  ? 6.441  5.527  9.850  1.00 4.13  ? ? ? ? ? ? 9  ALA A C   1 
+ATOM 58  O O   . ALA A 1 9  ? 6.052  5.933  8.744  1.00 4.36  ? ? ? ? ? ? 9  ALA A O   1 
+ATOM 59  C CB  . ALA A 1 9  ? 6.022  6.977  11.891 1.00 4.80  ? ? ? ? ? ? 9  ALA A CB  1 
+ATOM 60  N N   . ARG A 1 10 ? 7.647  4.909  10.005 1.00 3.73  ? ? ? ? ? ? 10 ARG A N   1 
+ATOM 61  C CA  . ARG A 1 10 ? 8.496  4.609  8.837  1.00 3.38  ? ? ? ? ? ? 10 ARG A CA  1 
+ATOM 62  C C   . ARG A 1 10 ? 7.798  3.609  7.876  1.00 3.47  ? ? ? ? ? ? 10 ARG A C   1 
+ATOM 63  O O   . ARG A 1 10 ? 7.878  3.778  6.651  1.00 4.67  ? ? ? ? ? ? 10 ARG A O   1 
+ATOM 64  C CB  . ARG A 1 10 ? 9.847  4.020  9.305  1.00 3.95  ? ? ? ? ? ? 10 ARG A CB  1 
+ATOM 65  C CG  . ARG A 1 10 ? 10.752 3.607  8.149  1.00 4.55  ? ? ? ? ? ? 10 ARG A CG  1 
+ATOM 66  C CD  . ARG A 1 10 ? 11.226 4.699  7.244  1.00 5.89  ? ? ? ? ? ? 10 ARG A CD  1 
+ATOM 67  N NE  . ARG A 1 10 ? 12.143 5.571  8.035  1.00 6.20  ? ? ? ? ? ? 10 ARG A NE  1 
+ATOM 68  C CZ  . ARG A 1 10 ? 12.758 6.609  7.443  1.00 7.52  ? ? ? ? ? ? 10 ARG A CZ  1 
+ATOM 69  N NH1 . ARG A 1 10 ? 12.539 6.932  6.158  1.00 10.68 ? ? ? ? ? ? 10 ARG A NH1 1 
+ATOM 70  N NH2 . ARG A 1 10 ? 13.601 7.322  8.202  1.00 9.48  ? ? ? ? ? ? 10 ARG A NH2 1 
+ATOM 71  N N   . SER A 1 11 ? 7.186  2.582  8.445  1.00 5.19  ? ? ? ? ? ? 11 SER A N   1 
+ATOM 72  C CA  . SER A 1 11 ? 6.500  1.584  7.565  1.00 4.60  ? ? ? ? ? ? 11 SER A CA  1 
+ATOM 73  C C   . SER A 1 11 ? 5.382  2.313  6.773  1.00 4.84  ? ? ? ? ? ? 11 SER A C   1 
+ATOM 74  O O   . SER A 1 11 ? 5.213  2.016  5.557  1.00 5.84  ? ? ? ? ? ? 11 SER A O   1 
+ATOM 75  C CB  . SER A 1 11 ? 5.908  0.462  8.400  1.00 5.91  ? ? ? ? ? ? 11 SER A CB  1 
+ATOM 76  O OG  . SER A 1 11 ? 6.990  -0.272 9.012  1.00 8.38  ? ? ? ? ? ? 11 SER A OG  1 
+ATOM 77  N N   . ASN A 1 12 ? 4.648  3.182  7.446  1.00 3.54  ? ? ? ? ? ? 12 ASN A N   1 
+ATOM 78  C CA  . ASN A 1 12 ? 3.545  3.935  6.751  1.00 4.57  ? ? ? ? ? ? 12 ASN A CA  1 
+ATOM 79  C C   . ASN A 1 12 ? 4.107  4.851  5.691  1.00 4.14  ? ? ? ? ? ? 12 ASN A C   1 
+ATOM 80  O O   . ASN A 1 12 ? 3.536  5.001  4.617  1.00 5.52  ? ? ? ? ? ? 12 ASN A O   1 
+ATOM 81  C CB  . ASN A 1 12 ? 2.663  4.677  7.748  1.00 6.42  ? ? ? ? ? ? 12 ASN A CB  1 
+ATOM 82  C CG  . ASN A 1 12 ? 1.802  3.735  8.610  1.00 8.25  ? ? ? ? ? ? 12 ASN A CG  1 
+ATOM 83  O OD1 . ASN A 1 12 ? 1.567  2.613  8.165  1.00 12.72 ? ? ? ? ? ? 12 ASN A OD1 1 
+ATOM 84  N ND2 . ASN A 1 12 ? 1.394  4.252  9.767  1.00 9.92  ? ? ? ? ? ? 12 ASN A ND2 1 
+ATOM 85  N N   . PHE A 1 13 ? 5.259  5.498  6.005  1.00 3.43  ? ? ? ? ? ? 13 PHE A N   1 
+ATOM 86  C CA  . PHE A 1 13 ? 5.929  6.358  5.055  1.00 3.49  ? ? ? ? ? ? 13 PHE A CA  1 
+ATOM 87  C C   . PHE A 1 13 ? 6.304  5.578  3.799  1.00 3.40  ? ? ? ? ? ? 13 PHE A C   1 
+ATOM 88  O O   . PHE A 1 13 ? 6.136  6.072  2.653  1.00 4.07  ? ? ? ? ? ? 13 PHE A O   1 
+ATOM 89  C CB  . PHE A 1 13 ? 7.183  6.994  5.754  1.00 5.48  ? ? ? ? ? ? 13 PHE A CB  1 
+ATOM 90  C CG  . PHE A 1 13 ? 7.884  8.006  4.883  1.00 5.57  ? ? ? ? ? ? 13 PHE A CG  1 
+ATOM 91  C CD1 . PHE A 1 13 ? 8.906  7.586  4.027  1.00 6.99  ? ? ? ? ? ? 13 PHE A CD1 1 
+ATOM 92  C CD2 . PHE A 1 13 ? 7.532  9.373  4.983  1.00 6.52  ? ? ? ? ? ? 13 PHE A CD2 1 
+ATOM 93  C CE1 . PHE A 1 13 ? 9.560  8.539  3.194  1.00 8.20  ? ? ? ? ? ? 13 PHE A CE1 1 
+ATOM 94  C CE2 . PHE A 1 13 ? 8.176  10.281 4.145  1.00 6.34  ? ? ? ? ? ? 13 PHE A CE2 1 
+ATOM 95  C CZ  . PHE A 1 13 ? 9.141  9.845  3.292  1.00 6.84  ? ? ? ? ? ? 13 PHE A CZ  1 
+ATOM 96  N N   . ASN A 1 14 ? 6.900  4.390  3.989  1.00 3.64  ? ? ? ? ? ? 14 ASN A N   1 
+ATOM 97  C CA  . ASN A 1 14 ? 7.331  3.607  2.791  1.00 4.31  ? ? ? ? ? ? 14 ASN A CA  1 
+ATOM 98  C C   . ASN A 1 14 ? 6.116  3.210  1.915  1.00 3.98  ? ? ? ? ? ? 14 ASN A C   1 
+ATOM 99  O O   . ASN A 1 14 ? 6.240  3.144  0.684  1.00 6.22  ? ? ? ? ? ? 14 ASN A O   1 
+ATOM 100 C CB  . ASN A 1 14 ? 8.145  2.404  3.240  1.00 5.81  ? ? ? ? ? ? 14 ASN A CB  1 
+ATOM 101 C CG  . ASN A 1 14 ? 9.555  2.856  3.730  1.00 6.82  ? ? ? ? ? ? 14 ASN A CG  1 
+ATOM 102 O OD1 . ASN A 1 14 ? 10.013 3.895  3.323  1.00 9.43  ? ? ? ? ? ? 14 ASN A OD1 1 
+ATOM 103 N ND2 . ASN A 1 14 ? 10.120 1.956  4.539  1.00 8.21  ? ? ? ? ? ? 14 ASN A ND2 1 
+ATOM 104 N N   . VAL A 1 15 ? 4.993  2.927  2.571  1.00 3.76  ? ? ? ? ? ? 15 VAL A N   1 
+ATOM 105 C CA  . VAL A 1 15 ? 3.782  2.599  1.742  1.00 3.98  ? ? ? ? ? ? 15 VAL A CA  1 
+ATOM 106 C C   . VAL A 1 15 ? 3.296  3.871  1.004  1.00 3.80  ? ? ? ? ? ? 15 VAL A C   1 
+ATOM 107 O O   . VAL A 1 15 ? 2.947  3.817  -0.189 1.00 4.85  ? ? ? ? ? ? 15 VAL A O   1 
+ATOM 108 C CB  . VAL A 1 15 ? 2.698  1.953  2.608  1.00 4.71  ? ? ? ? ? ? 15 VAL A CB  1 
+ATOM 109 C CG1 . VAL A 1 15 ? 1.384  1.826  1.806  1.00 6.67  ? ? ? ? ? ? 15 VAL A CG1 1 
+ATOM 110 C CG2 . VAL A 1 15 ? 3.174  0.533  3.005  1.00 6.26  ? ? ? ? ? ? 15 VAL A CG2 1 
+ATOM 111 N N   . CYS A 1 16 ? 3.321  4.987  1.720  1.00 3.79  ? ? ? ? ? ? 16 CYS A N   1 
+ATOM 112 C CA  . CYS A 1 16 ? 2.890  6.285  1.126  1.00 3.54  ? ? ? ? ? ? 16 CYS A CA  1 
+ATOM 113 C C   . CYS A 1 16 ? 3.687  6.597  -0.111 1.00 3.48  ? ? ? ? ? ? 16 CYS A C   1 
+ATOM 114 O O   . CYS A 1 16 ? 3.200  7.147  -1.103 1.00 4.63  ? ? ? ? ? ? 16 CYS A O   1 
+ATOM 115 C CB  . CYS A 1 16 ? 3.039  7.369  2.240  1.00 4.58  ? ? ? ? ? ? 16 CYS A CB  1 
+ATOM 116 S SG  . CYS A 1 16 ? 2.559  9.014  1.649  1.00 5.66  ? ? ? ? ? ? 16 CYS A SG  1 
+ATOM 117 N N   . ARG A 1 17 ? 4.997  6.227  -0.100 1.00 3.99  ? ? ? ? ? ? 17 ARG A N   1 
+ATOM 118 C CA  . ARG A 1 17 ? 5.895  6.489  -1.213 1.00 3.83  ? ? ? ? ? ? 17 ARG A CA  1 
+ATOM 119 C C   . ARG A 1 17 ? 5.738  5.560  -2.409 1.00 3.79  ? ? ? ? ? ? 17 ARG A C   1 
+ATOM 120 O O   . ARG A 1 17 ? 6.228  5.901  -3.507 1.00 5.39  ? ? ? ? ? ? 17 ARG A O   1 
+ATOM 121 C CB  . ARG A 1 17 ? 7.370  6.507  -0.731 1.00 4.11  ? ? ? ? ? ? 17 ARG A CB  1 
+ATOM 122 C CG  . ARG A 1 17 ? 7.717  7.687  0.206  1.00 4.69  ? ? ? ? ? ? 17 ARG A CG  1 
+ATOM 123 C CD  . ARG A 1 17 ? 7.949  8.947  -0.615 1.00 5.10  ? ? ? ? ? ? 17 ARG A CD  1 
+ATOM 124 N NE  . ARG A 1 17 ? 9.212  8.856  -1.337 1.00 4.71  ? ? ? ? ? ? 17 ARG A NE  1 
+ATOM 125 C CZ  . ARG A 1 17 ? 9.537  9.533  -2.431 1.00 5.28  ? ? ? ? ? ? 17 ARG A CZ  1 
+ATOM 126 N NH1 . ARG A 1 17 ? 8.659  10.350 -3.032 1.00 6.67  ? ? ? ? ? ? 17 ARG A NH1 1 
+ATOM 127 N NH2 . ARG A 1 17 ? 10.793 9.491  -2.899 1.00 6.41  ? ? ? ? ? ? 17 ARG A NH2 1 
+ATOM 128 N N   . LEU A 1 18 ? 5.051  4.411  -2.204 1.00 4.70  ? ? ? ? ? ? 18 LEU A N   1 
+ATOM 129 C CA  . LEU A 1 18 ? 4.933  3.431  -3.326 1.00 5.46  ? ? ? ? ? ? 18 LEU A CA  1 
+ATOM 130 C C   . LEU A 1 18 ? 4.397  4.014  -4.620 1.00 5.13  ? ? ? ? ? ? 18 LEU A C   1 
+ATOM 131 O O   . LEU A 1 18 ? 4.988  3.755  -5.687 1.00 5.55  ? ? ? ? ? ? 18 LEU A O   1 
+ATOM 132 C CB  . LEU A 1 18 ? 4.196  2.184  -2.863 1.00 6.47  ? ? ? ? ? ? 18 LEU A CB  1 
+ATOM 133 C CG  . LEU A 1 18 ? 4.960  1.178  -1.991 1.00 7.43  ? ? ? ? ? ? 18 LEU A CG  1 
+ATOM 134 C CD1 . LEU A 1 18 ? 3.907  0.097  -1.634 1.00 8.70  ? ? ? ? ? ? 18 LEU A CD1 1 
+ATOM 135 C CD2 . LEU A 1 18 ? 6.129  0.606  -2.768 1.00 9.39  ? ? ? ? ? ? 18 LEU A CD2 1 
+ATOM 136 N N   . PRO A 1 19 ? 3.329  4.795  -4.543 1.00 4.28  ? ? ? ? ? ? 19 PRO A N   1 
+ATOM 137 C CA  . PRO A 1 19 ? 2.792  5.376  -5.797 1.00 5.38  ? ? ? ? ? ? 19 PRO A CA  1 
+ATOM 138 C C   . PRO A 1 19 ? 3.573  6.540  -6.322 1.00 6.30  ? ? ? ? ? ? 19 PRO A C   1 
+ATOM 139 O O   . PRO A 1 19 ? 3.260  7.045  -7.422 1.00 9.62  ? ? ? ? ? ? 19 PRO A O   1 
+ATOM 140 C CB  . PRO A 1 19 ? 1.358  5.766  -5.472 1.00 5.87  ? ? ? ? ? ? 19 PRO A CB  1 
+ATOM 141 C CG  . PRO A 1 19 ? 1.223  5.694  -3.993 1.00 6.47  ? ? ? ? ? ? 19 PRO A CG  1 
+ATOM 142 C CD  . PRO A 1 19 ? 2.421  4.941  -3.408 1.00 6.45  ? ? ? ? ? ? 19 PRO A CD  1 
+ATOM 143 N N   . GLY A 1 20 ? 4.565  7.047  -5.559 1.00 4.94  ? ? ? ? ? ? 20 GLY A N   1 
+ATOM 144 C CA  . GLY A 1 20 ? 5.366  8.191  -6.018 1.00 5.39  ? ? ? ? ? ? 20 GLY A CA  1 
+ATOM 145 C C   . GLY A 1 20 ? 5.007  9.481  -5.280 1.00 5.03  ? ? ? ? ? ? 20 GLY A C   1 
+ATOM 146 O O   . GLY A 1 20 ? 5.535  10.510 -5.730 1.00 7.34  ? ? ? ? ? ? 20 GLY A O   1 
+ATOM 147 N N   . THR A 1 21 ? 4.181  9.438  -4.262 1.00 4.10  ? ? ? ? ? ? 21 THR A N   1 
+ATOM 148 C CA  . THR A 1 21 ? 3.767  10.609 -3.513 1.00 3.94  ? ? ? ? ? ? 21 THR A CA  1 
+ATOM 149 C C   . THR A 1 21 ? 5.017  11.397 -3.042 1.00 3.96  ? ? ? ? ? ? 21 THR A C   1 
+ATOM 150 O O   . THR A 1 21 ? 5.947  10.757 -2.523 1.00 5.82  ? ? ? ? ? ? 21 THR A O   1 
+ATOM 151 C CB  . THR A 1 21 ? 2.992  10.188 -2.225 1.00 4.13  ? ? ? ? ? ? 21 THR A CB  1 
+ATOM 152 O OG1 . THR A 1 21 ? 2.051  9.144  -2.623 1.00 5.45  ? ? ? ? ? ? 21 THR A OG1 1 
+ATOM 153 C CG2 . THR A 1 21 ? 2.260  11.349 -1.551 1.00 5.41  ? ? ? ? ? ? 21 THR A CG2 1 
+ATOM 154 N N   . PRO A 1 22 ? 4.971  12.703 -3.176 1.00 5.04  ? ? ? ? ? ? 22 PRO A N   1 
+ATOM 155 C CA  . PRO A 1 22 ? 6.143  13.513 -2.696 1.00 4.69  ? ? ? ? ? ? 22 PRO A CA  1 
+ATOM 156 C C   . PRO A 1 22 ? 6.400  13.233 -1.225 1.00 4.19  ? ? ? ? ? ? 22 PRO A C   1 
+ATOM 157 O O   . PRO A 1 22 ? 5.485  13.061 -0.382 1.00 4.47  ? ? ? ? ? ? 22 PRO A O   1 
+ATOM 158 C CB  . PRO A 1 22 ? 5.703  14.969 -2.920 1.00 7.12  ? ? ? ? ? ? 22 PRO A CB  1 
+ATOM 159 C CG  . PRO A 1 22 ? 4.676  14.893 -3.996 1.00 7.03  ? ? ? ? ? ? 22 PRO A CG  1 
+ATOM 160 C CD  . PRO A 1 22 ? 3.964  13.567 -3.811 1.00 4.90  ? ? ? ? ? ? 22 PRO A CD  1 
+ATOM 161 N N   . GLU A 1 23 ? 7.728  13.297 -0.921 1.00 5.16  ? ? ? ? ? ? 23 GLU A N   1 
+ATOM 162 C CA  . GLU A 1 23 ? 8.114  13.103 0.500  1.00 5.31  ? ? ? ? ? ? 23 GLU A CA  1 
+ATOM 163 C C   . GLU A 1 23 ? 7.427  14.073 1.410  1.00 4.11  ? ? ? ? ? ? 23 GLU A C   1 
+ATOM 164 O O   . GLU A 1 23 ? 7.036  13.682 2.540  1.00 5.11  ? ? ? ? ? ? 23 GLU A O   1 
+ATOM 165 C CB  . GLU A 1 23 ? 9.648  13.285 0.660  1.00 6.16  ? ? ? ? ? ? 23 GLU A CB  1 
+ATOM 166 C CG  . GLU A 1 23 ? 10.440 12.093 0.063  1.00 7.48  ? ? ? ? ? ? 23 GLU A CG  1 
+ATOM 167 C CD  . GLU A 1 23 ? 11.941 12.170 0.391  1.00 9.40  ? ? ? ? ? ? 23 GLU A CD  1 
+ATOM 168 O OE1 . GLU A 1 23 ? 12.416 13.225 0.681  1.00 10.40 ? ? ? ? ? ? 23 GLU A OE1 1 
+ATOM 169 O OE2 . GLU A 1 23 ? 12.539 11.070 0.292  1.00 13.32 ? ? ? ? ? ? 23 GLU A OE2 1 
+ATOM 170 N N   . ALA A 1 24 ? 7.212  15.334 0.966  1.00 4.56  ? ? ? ? ? ? 24 ALA A N   1 
+ATOM 171 C CA  . ALA A 1 24 ? 6.614  16.317 1.913  1.00 4.49  ? ? ? ? ? ? 24 ALA A CA  1 
+ATOM 172 C C   . ALA A 1 24 ? 5.212  15.936 2.350  1.00 4.10  ? ? ? ? ? ? 24 ALA A C   1 
+ATOM 173 O O   . ALA A 1 24 ? 4.782  16.166 3.495  1.00 5.64  ? ? ? ? ? ? 24 ALA A O   1 
+ATOM 174 C CB  . ALA A 1 24 ? 6.605  17.695 1.246  1.00 5.80  ? ? ? ? ? ? 24 ALA A CB  1 
+ATOM 175 N N   . ILE A 1 25 ? 4.445  15.318 1.405  1.00 4.37  ? ? ? ? ? ? 25 ILE A N   1 
+ATOM 176 C CA  . ILE A 1 25 ? 3.074  14.894 1.756  1.00 5.44  ? ? ? ? ? ? 25 ILE A CA  1 
+ATOM 177 C C   . ILE A 1 25 ? 3.085  13.643 2.645  1.00 4.32  ? ? ? ? ? ? 25 ILE A C   1 
+ATOM 178 O O   . ILE A 1 25 ? 2.315  13.523 3.578  1.00 4.72  ? ? ? ? ? ? 25 ILE A O   1 
+ATOM 179 C CB  . ILE A 1 25 ? 2.204  14.637 0.462  1.00 6.42  ? ? ? ? ? ? 25 ILE A CB  1 
+ATOM 180 C CG1 . ILE A 1 25 ? 1.815  16.048 -0.129 1.00 7.50  ? ? ? ? ? ? 25 ILE A CG1 1 
+ATOM 181 C CG2 . ILE A 1 25 ? 0.903  13.864 0.811  1.00 7.65  ? ? ? ? ? ? 25 ILE A CG2 1 
+ATOM 182 C CD1 . ILE A 1 25 ? 0.756  16.761 0.757  1.00 7.80  ? ? ? ? ? ? 25 ILE A CD1 1 
+ATOM 183 N N   . CYS A 1 26 ? 4.032  12.764 2.313  1.00 3.92  ? ? ? ? ? ? 26 CYS A N   1 
+ATOM 184 C CA  . CYS A 1 26 ? 4.180  11.549 3.187  1.00 4.37  ? ? ? ? ? ? 26 CYS A CA  1 
+ATOM 185 C C   . CYS A 1 26 ? 4.632  11.944 4.596  1.00 3.95  ? ? ? ? ? ? 26 CYS A C   1 
+ATOM 186 O O   . CYS A 1 26 ? 4.227  11.252 5.547  1.00 4.74  ? ? ? ? ? ? 26 CYS A O   1 
+ATOM 187 C CB  . CYS A 1 26 ? 5.038  10.518 2.539  1.00 4.63  ? ? ? ? ? ? 26 CYS A CB  1 
+ATOM 188 S SG  . CYS A 1 26 ? 4.349  9.794  1.022  1.00 5.61  ? ? ? ? ? ? 26 CYS A SG  1 
+ATOM 189 N N   . ALA A 1 27 ? 5.408  13.012 4.694  1.00 3.89  ? ? ? ? ? ? 27 ALA A N   1 
+ATOM 190 C CA  . ALA A 1 27 ? 5.879  13.502 6.026  1.00 4.43  ? ? ? ? ? ? 27 ALA A CA  1 
+ATOM 191 C C   . ALA A 1 27 ? 4.696  13.908 6.882  1.00 4.26  ? ? ? ? ? ? 27 ALA A C   1 
+ATOM 192 O O   . ALA A 1 27 ? 4.528  13.422 8.025  1.00 5.44  ? ? ? ? ? ? 27 ALA A O   1 
+ATOM 193 C CB  . ALA A 1 27 ? 6.880  14.615 5.830  1.00 5.36  ? ? ? ? ? ? 27 ALA A CB  1 
+ATOM 194 N N   . THR A 1 28 ? 3.827  14.802 6.358  1.00 4.53  ? ? ? ? ? ? 28 THR A N   1 
+ATOM 195 C CA  . THR A 1 28 ? 2.691  15.221 7.194  1.00 5.08  ? ? ? ? ? ? 28 THR A CA  1 
+ATOM 196 C C   . THR A 1 28 ? 1.672  14.132 7.434  1.00 4.62  ? ? ? ? ? ? 28 THR A C   1 
+ATOM 197 O O   . THR A 1 28 ? 0.947  14.112 8.468  1.00 7.80  ? ? ? ? ? ? 28 THR A O   1 
+ATOM 198 C CB  . THR A 1 28 ? 1.986  16.520 6.614  1.00 6.03  ? ? ? ? ? ? 28 THR A CB  1 
+ATOM 199 O OG1 . THR A 1 28 ? 1.664  16.221 5.230  1.00 7.19  ? ? ? ? ? ? 28 THR A OG1 1 
+ATOM 200 C CG2 . THR A 1 28 ? 2.914  17.739 6.700  1.00 7.34  ? ? ? ? ? ? 28 THR A CG2 1 
+ATOM 201 N N   . TYR A 1 29 ? 1.621  13.190 6.511  1.00 5.01  ? ? ? ? ? ? 29 TYR A N   1 
+ATOM 202 C CA  . TYR A 1 29 ? 0.715  12.045 6.657  1.00 6.60  ? ? ? ? ? ? 29 TYR A CA  1 
+ATOM 203 C C   . TYR A 1 29 ? 1.125  11.125 7.815  1.00 4.92  ? ? ? ? ? ? 29 TYR A C   1 
+ATOM 204 O O   . TYR A 1 29 ? 0.286  10.632 8.545  1.00 7.13  ? ? ? ? ? ? 29 TYR A O   1 
+ATOM 205 C CB  . TYR A 1 29 ? 0.755  11.229 5.322  1.00 9.66  ? ? ? ? ? ? 29 TYR A CB  1 
+ATOM 206 C CG  . TYR A 1 29 ? -0.203 10.044 5.354  1.00 11.56 ? ? ? ? ? ? 29 TYR A CG  1 
+ATOM 207 C CD1 . TYR A 1 29 ? -1.547 10.337 5.645  1.00 12.85 ? ? ? ? ? ? 29 TYR A CD1 1 
+ATOM 208 C CD2 . TYR A 1 29 ? 0.193  8.750  5.100  1.00 14.44 ? ? ? ? ? ? 29 TYR A CD2 1 
+ATOM 209 C CE1 . TYR A 1 29 ? -2.496 9.329  5.673  1.00 16.61 ? ? ? ? ? ? 29 TYR A CE1 1 
+ATOM 210 C CE2 . TYR A 1 29 ? -0.801 7.705  5.156  1.00 17.11 ? ? ? ? ? ? 29 TYR A CE2 1 
+ATOM 211 C CZ  . TYR A 1 29 ? -2.079 8.031  5.430  1.00 19.99 ? ? ? ? ? ? 29 TYR A CZ  1 
+ATOM 212 O OH  . TYR A 1 29 ? -3.097 7.057  5.458  1.00 28.98 ? ? ? ? ? ? 29 TYR A OH  1 
+ATOM 213 N N   . THR A 1 30 ? 2.470  10.984 7.995  1.00 5.31  ? ? ? ? ? ? 30 THR A N   1 
+ATOM 214 C CA  . THR A 1 30 ? 2.986  9.994  8.950  1.00 5.70  ? ? ? ? ? ? 30 THR A CA  1 
+ATOM 215 C C   . THR A 1 30 ? 3.609  10.505 10.230 1.00 6.28  ? ? ? ? ? ? 30 THR A C   1 
+ATOM 216 O O   . THR A 1 30 ? 3.766  9.715  11.186 1.00 8.77  ? ? ? ? ? ? 30 THR A O   1 
+ATOM 217 C CB  . THR A 1 30 ? 4.076  9.103  8.225  1.00 6.55  ? ? ? ? ? ? 30 THR A CB  1 
+ATOM 218 O OG1 . THR A 1 30 ? 5.125  10.027 7.824  1.00 6.57  ? ? ? ? ? ? 30 THR A OG1 1 
+ATOM 219 C CG2 . THR A 1 30 ? 3.493  8.324  7.035  1.00 7.29  ? ? ? ? ? ? 30 THR A CG2 1 
+ATOM 220 N N   . GLY A 1 31 ? 3.984  11.764 10.241 1.00 4.99  ? ? ? ? ? ? 31 GLY A N   1 
+ATOM 221 C CA  . GLY A 1 31 ? 4.769  12.336 11.360 1.00 5.50  ? ? ? ? ? ? 31 GLY A CA  1 
+ATOM 222 C C   . GLY A 1 31 ? 6.255  12.243 11.106 1.00 4.19  ? ? ? ? ? ? 31 GLY A C   1 
+ATOM 223 O O   . GLY A 1 31 ? 7.037  12.750 11.954 1.00 6.12  ? ? ? ? ? ? 31 GLY A O   1 
+ATOM 224 N N   . CYS A 1 32 ? 6.710  11.631 9.992  1.00 4.30  ? ? ? ? ? ? 32 CYS A N   1 
+ATOM 225 C CA  . CYS A 1 32 ? 8.140  11.694 9.635  1.00 4.89  ? ? ? ? ? ? 32 CYS A CA  1 
+ATOM 226 C C   . CYS A 1 32 ? 8.500  13.141 9.206  1.00 5.50  ? ? ? ? ? ? 32 CYS A C   1 
+ATOM 227 O O   . CYS A 1 32 ? 7.581  13.949 8.944  1.00 5.82  ? ? ? ? ? ? 32 CYS A O   1 
+ATOM 228 C CB  . CYS A 1 32 ? 8.504  10.686 8.530  1.00 4.66  ? ? ? ? ? ? 32 CYS A CB  1 
+ATOM 229 S SG  . CYS A 1 32 ? 8.048  8.987  8.881  1.00 5.33  ? ? ? ? ? ? 32 CYS A SG  1 
+ATOM 230 N N   . ILE A 1 33 ? 9.793  13.410 9.173  1.00 6.02  ? ? ? ? ? ? 33 ILE A N   1 
+ATOM 231 C CA  . ILE A 1 33 ? 10.280 14.760 8.823  1.00 5.24  ? ? ? ? ? ? 33 ILE A CA  1 
+ATOM 232 C C   . ILE A 1 33 ? 11.346 14.658 7.743  1.00 5.16  ? ? ? ? ? ? 33 ILE A C   1 
+ATOM 233 O O   . ILE A 1 33 ? 11.971 13.583 7.552  1.00 7.19  ? ? ? ? ? ? 33 ILE A O   1 
+ATOM 234 C CB  . ILE A 1 33 ? 10.790 15.535 10.085 1.00 5.49  ? ? ? ? ? ? 33 ILE A CB  1 
+ATOM 235 C CG1 . ILE A 1 33 ? 12.059 14.803 10.671 1.00 6.85  ? ? ? ? ? ? 33 ILE A CG1 1 
+ATOM 236 C CG2 . ILE A 1 33 ? 9.684  15.686 11.138 1.00 6.45  ? ? ? ? ? ? 33 ILE A CG2 1 
+ATOM 237 C CD1 . ILE A 1 33 ? 12.733 15.676 11.781 1.00 8.94  ? ? ? ? ? ? 33 ILE A CD1 1 
+ATOM 238 N N   . ILE A 1 34 ? 11.490 15.773 7.038  1.00 5.52  ? ? ? ? ? ? 34 ILE A N   1 
+ATOM 239 C CA  . ILE A 1 34 ? 12.552 15.877 6.036  1.00 6.82  ? ? ? ? ? ? 34 ILE A CA  1 
+ATOM 240 C C   . ILE A 1 34 ? 13.590 16.917 6.560  1.00 6.92  ? ? ? ? ? ? 34 ILE A C   1 
+ATOM 241 O O   . ILE A 1 34 ? 13.168 18.006 6.945  1.00 9.22  ? ? ? ? ? ? 34 ILE A O   1 
+ATOM 242 C CB  . ILE A 1 34 ? 11.987 16.360 4.681  1.00 8.11  ? ? ? ? ? ? 34 ILE A CB  1 
+ATOM 243 C CG1 . ILE A 1 34 ? 10.914 15.338 4.163  1.00 9.59  ? ? ? ? ? ? 34 ILE A CG1 1 
+ATOM 244 C CG2 . ILE A 1 34 ? 13.131 16.517 3.629  1.00 9.73  ? ? ? ? ? ? 34 ILE A CG2 1 
+ATOM 245 C CD1 . ILE A 1 34 ? 10.151 16.024 2.938  1.00 13.41 ? ? ? ? ? ? 34 ILE A CD1 1 
+ATOM 246 N N   . ILE A 1 35 ? 14.856 16.493 6.536  1.00 7.06  ? ? ? ? ? ? 35 ILE A N   1 
+ATOM 247 C CA  . ILE A 1 35 ? 15.930 17.454 6.941  1.00 7.52  ? ? ? ? ? ? 35 ILE A CA  1 
+ATOM 248 C C   . ILE A 1 35 ? 16.913 17.550 5.819  1.00 6.63  ? ? ? ? ? ? 35 ILE A C   1 
+ATOM 249 O O   . ILE A 1 35 ? 17.097 16.660 4.970  1.00 7.90  ? ? ? ? ? ? 35 ILE A O   1 
+ATOM 250 C CB  . ILE A 1 35 ? 16.622 16.995 8.285  1.00 8.07  ? ? ? ? ? ? 35 ILE A CB  1 
+ATOM 251 C CG1 . ILE A 1 35 ? 17.360 15.651 8.067  1.00 9.41  ? ? ? ? ? ? 35 ILE A CG1 1 
+ATOM 252 C CG2 . ILE A 1 35 ? 15.592 16.974 9.434  1.00 9.46  ? ? ? ? ? ? 35 ILE A CG2 1 
+ATOM 253 C CD1 . ILE A 1 35 ? 18.298 15.206 9.219  1.00 9.85  ? ? ? ? ? ? 35 ILE A CD1 1 
+ATOM 254 N N   . PRO A 1 36 ? 17.664 18.669 5.806  1.00 8.07  ? ? ? ? ? ? 36 PRO A N   1 
+ATOM 255 C CA  . PRO A 1 36 ? 18.635 18.861 4.738  1.00 8.78  ? ? ? ? ? ? 36 PRO A CA  1 
+ATOM 256 C C   . PRO A 1 36 ? 19.925 18.042 4.949  1.00 8.31  ? ? ? ? ? ? 36 PRO A C   1 
+ATOM 257 O O   . PRO A 1 36 ? 20.593 17.742 3.945  1.00 9.09  ? ? ? ? ? ? 36 PRO A O   1 
+ATOM 258 C CB  . PRO A 1 36 ? 18.945 20.364 4.783  1.00 9.67  ? ? ? ? ? ? 36 PRO A CB  1 
+ATOM 259 C CG  . PRO A 1 36 ? 18.238 20.937 5.908  1.00 10.15 ? ? ? ? ? ? 36 PRO A CG  1 
+ATOM 260 C CD  . PRO A 1 36 ? 17.371 19.900 6.596  1.00 9.53  ? ? ? ? ? ? 36 PRO A CD  1 
+ATOM 261 N N   . GLY A 1 37 ? 20.172 17.730 6.217  1.00 8.48  ? ? ? ? ? ? 37 GLY A N   1 
+ATOM 262 C CA  . GLY A 1 37 ? 21.452 16.969 6.513  1.00 9.20  ? ? ? ? ? ? 37 GLY A CA  1 
+ATOM 263 C C   . GLY A 1 37 ? 21.143 15.478 6.427  1.00 10.41 ? ? ? ? ? ? 37 GLY A C   1 
+ATOM 264 O O   . GLY A 1 37 ? 20.138 15.023 5.878  1.00 12.06 ? ? ? ? ? ? 37 GLY A O   1 
+ATOM 265 N N   . ALA A 1 38 ? 22.055 14.701 7.032  1.00 9.24  ? ? ? ? ? ? 38 ALA A N   1 
+ATOM 266 C CA  . ALA A 1 38 ? 22.019 13.242 7.020  1.00 9.24  ? ? ? ? ? ? 38 ALA A CA  1 
+ATOM 267 C C   . ALA A 1 38 ? 21.944 12.628 8.396  1.00 9.60  ? ? ? ? ? ? 38 ALA A C   1 
+ATOM 268 O O   . ALA A 1 38 ? 21.869 11.387 8.435  1.00 13.65 ? ? ? ? ? ? 38 ALA A O   1 
+ATOM 269 C CB  . ALA A 1 38 ? 23.246 12.697 6.275  1.00 10.43 ? ? ? ? ? ? 38 ALA A CB  1 
+ATOM 270 N N   . THR A 1 39 ? 21.894 13.435 9.436  1.00 8.70  ? ? ? ? ? ? 39 THR A N   1 
+ATOM 271 C CA  . THR A 1 39 ? 21.936 12.911 10.809 1.00 9.46  ? ? ? ? ? ? 39 THR A CA  1 
+ATOM 272 C C   . THR A 1 39 ? 20.615 13.191 11.521 1.00 8.32  ? ? ? ? ? ? 39 THR A C   1 
+ATOM 273 O O   . THR A 1 39 ? 20.357 14.317 11.948 1.00 9.89  ? ? ? ? ? ? 39 THR A O   1 
+ATOM 274 C CB  . THR A 1 39 ? 23.131 13.601 11.593 1.00 10.72 ? ? ? ? ? ? 39 THR A CB  1 
+ATOM 275 O OG1 . THR A 1 39 ? 24.284 13.401 10.709 1.00 11.66 ? ? ? ? ? ? 39 THR A OG1 1 
+ATOM 276 C CG2 . THR A 1 39 ? 23.340 12.935 12.962 1.00 11.81 ? ? ? ? ? ? 39 THR A CG2 1 
+ATOM 277 N N   . CYS A 1 40 ? 19.827 12.110 11.642 1.00 7.64  ? ? ? ? ? ? 40 CYS A N   1 
+ATOM 278 C CA  . CYS A 1 40 ? 18.504 12.312 12.298 1.00 8.05  ? ? ? ? ? ? 40 CYS A CA  1 
+ATOM 279 C C   . CYS A 1 40 ? 18.684 12.451 13.784 1.00 7.63  ? ? ? ? ? ? 40 CYS A C   1 
+ATOM 280 O O   . CYS A 1 40 ? 19.533 11.718 14.362 1.00 9.64  ? ? ? ? ? ? 40 CYS A O   1 
+ATOM 281 C CB  . CYS A 1 40 ? 17.582 11.117 11.996 1.00 7.80  ? ? ? ? ? ? 40 CYS A CB  1 
+ATOM 282 S SG  . CYS A 1 40 ? 17.199 10.929 10.237 1.00 7.30  ? ? ? ? ? ? 40 CYS A SG  1 
+ATOM 283 N N   . PRO A 1 41 ? 17.880 13.266 14.426 1.00 8.00  ? ? ? ? ? ? 41 PRO A N   1 
+ATOM 284 C CA  . PRO A 1 41 ? 17.924 13.421 15.877 1.00 8.96  ? ? ? ? ? ? 41 PRO A CA  1 
+ATOM 285 C C   . PRO A 1 41 ? 17.392 12.206 16.594 1.00 9.06  ? ? ? ? ? ? 41 PRO A C   1 
+ATOM 286 O O   . PRO A 1 41 ? 16.652 11.368 16.033 1.00 8.82  ? ? ? ? ? ? 41 PRO A O   1 
+ATOM 287 C CB  . PRO A 1 41 ? 17.076 14.658 16.145 1.00 10.39 ? ? ? ? ? ? 41 PRO A CB  1 
+ATOM 288 C CG  . PRO A 1 41 ? 16.098 14.689 14.997 1.00 10.99 ? ? ? ? ? ? 41 PRO A CG  1 
+ATOM 289 C CD  . PRO A 1 41 ? 16.859 14.150 13.779 1.00 10.49 ? ? ? ? ? ? 41 PRO A CD  1 
+ATOM 290 N N   . GLY A 1 42 ? 17.728 12.124 17.884 1.00 7.55  ? ? ? ? ? ? 42 GLY A N   1 
+ATOM 291 C CA  . GLY A 1 42 ? 17.334 10.956 18.691 1.00 8.00  ? ? ? ? ? ? 42 GLY A CA  1 
+ATOM 292 C C   . GLY A 1 42 ? 15.875 10.688 18.871 1.00 7.22  ? ? ? ? ? ? 42 GLY A C   1 
+ATOM 293 O O   . GLY A 1 42 ? 15.434 9.550  19.166 1.00 8.41  ? ? ? ? ? ? 42 GLY A O   1 
+ATOM 294 N N   . ASP A 1 43 ? 15.036 11.747 18.715 1.00 5.54  ? ? ? ? ? ? 43 ASP A N   1 
+ATOM 295 C CA  . ASP A 1 43 ? 13.564 11.573 18.836 1.00 5.85  ? ? ? ? ? ? 43 ASP A CA  1 
+ATOM 296 C C   . ASP A 1 43 ? 12.936 11.227 17.470 1.00 5.87  ? ? ? ? ? ? 43 ASP A C   1 
+ATOM 297 O O   . ASP A 1 43 ? 11.720 11.040 17.428 1.00 7.29  ? ? ? ? ? ? 43 ASP A O   1 
+ATOM 298 C CB  . ASP A 1 43 ? 12.933 12.737 19.580 1.00 6.72  ? ? ? ? ? ? 43 ASP A CB  1 
+ATOM 299 C CG  . ASP A 1 43 ? 13.140 14.094 18.958 1.00 8.59  ? ? ? ? ? ? 43 ASP A CG  1 
+ATOM 300 O OD1 . ASP A 1 43 ? 14.109 14.303 18.212 1.00 9.59  ? ? ? ? ? ? 43 ASP A OD1 1 
+ATOM 301 O OD2 . ASP A 1 43 ? 12.267 14.963 19.265 1.00 11.45 ? ? ? ? ? ? 43 ASP A OD2 1 
+ATOM 302 N N   . TYR A 1 44 ? 13.725 11.174 16.425 1.00 5.22  ? ? ? ? ? ? 44 TYR A N   1 
+ATOM 303 C CA  . TYR A 1 44 ? 13.257 10.745 15.081 1.00 5.56  ? ? ? ? ? ? 44 TYR A CA  1 
+ATOM 304 C C   . TYR A 1 44 ? 14.275 9.687  14.612 1.00 4.61  ? ? ? ? ? ? 44 TYR A C   1 
+ATOM 305 O O   . TYR A 1 44 ? 14.930 9.862  13.568 1.00 6.04  ? ? ? ? ? ? 44 TYR A O   1 
+ATOM 306 C CB  . TYR A 1 44 ? 13.200 11.914 14.071 1.00 5.41  ? ? ? ? ? ? 44 TYR A CB  1 
+ATOM 307 C CG  . TYR A 1 44 ? 12.000 12.819 14.399 1.00 5.34  ? ? ? ? ? ? 44 TYR A CG  1 
+ATOM 308 C CD1 . TYR A 1 44 ? 12.119 13.853 15.332 1.00 6.59  ? ? ? ? ? ? 44 TYR A CD1 1 
+ATOM 309 C CD2 . TYR A 1 44 ? 10.775 12.617 13.762 1.00 5.94  ? ? ? ? ? ? 44 TYR A CD2 1 
+ATOM 310 C CE1 . TYR A 1 44 ? 11.045 14.675 15.610 1.00 5.97  ? ? ? ? ? ? 44 TYR A CE1 1 
+ATOM 311 C CE2 . TYR A 1 44 ? 9.676  13.433 14.048 1.00 5.17  ? ? ? ? ? ? 44 TYR A CE2 1 
+ATOM 312 C CZ  . TYR A 1 44 ? 9.802  14.456 14.996 1.00 5.96  ? ? ? ? ? ? 44 TYR A CZ  1 
+ATOM 313 O OH  . TYR A 1 44 ? 8.740  15.265 15.269 1.00 8.60  ? ? ? ? ? ? 44 TYR A OH  1 
+ATOM 314 N N   . ALA A 1 45 ? 14.342 8.640  15.422 1.00 4.76  ? ? ? ? ? ? 45 ALA A N   1 
+ATOM 315 C CA  . ALA A 1 45 ? 15.445 7.667  15.246 1.00 5.89  ? ? ? ? ? ? 45 ALA A CA  1 
+ATOM 316 C C   . ALA A 1 45 ? 15.171 6.533  14.280 1.00 6.67  ? ? ? ? ? ? 45 ALA A C   1 
+ATOM 317 O O   . ALA A 1 45 ? 16.093 5.705  14.039 1.00 7.56  ? ? ? ? ? ? 45 ALA A O   1 
+ATOM 318 C CB  . ALA A 1 45 ? 15.680 7.099  16.682 1.00 6.82  ? ? ? ? ? ? 45 ALA A CB  1 
+ATOM 319 N N   . ASN A 1 46 ? 13.966 6.502  13.739 1.00 5.80  ? ? ? ? ? ? 46 ASN A N   1 
+ATOM 320 C CA  . ASN A 1 46 ? 13.512 5.395  12.878 1.00 6.15  ? ? ? ? ? ? 46 ASN A CA  1 
+ATOM 321 C C   . ASN A 1 46 ? 13.311 5.853  11.455 1.00 6.61  ? ? ? ? ? ? 46 ASN A C   1 
+ATOM 322 O O   . ASN A 1 46 ? 13.733 6.929  11.026 1.00 7.18  ? ? ? ? ? ? 46 ASN A O   1 
+ATOM 323 C CB  . ASN A 1 46 ? 12.266 4.769  13.501 1.00 7.27  ? ? ? ? ? ? 46 ASN A CB  1 
+ATOM 324 C CG  . ASN A 1 46 ? 12.538 4.304  14.922 1.00 7.98  ? ? ? ? ? ? 46 ASN A CG  1 
+ATOM 325 O OD1 . ASN A 1 46 ? 11.982 4.849  15.886 1.00 11.00 ? ? ? ? ? ? 46 ASN A OD1 1 
+ATOM 326 N ND2 . ASN A 1 46 ? 13.407 3.298  15.015 1.00 10.32 ? ? ? ? ? ? 46 ASN A ND2 1 
+ATOM 327 O OXT . ASN A 1 46 ? 12.703 4.973  10.746 1.00 7.86  ? ? ? ? ? ? 46 ASN A OXT 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  THR 1  1  1  THR THR A . n 
+A 1 2  THR 2  2  2  THR THR A . n 
+A 1 3  CYS 3  3  3  CYS CYS A . n 
+A 1 4  CYS 4  4  4  CYS CYS A . n 
+A 1 5  PRO 5  5  5  PRO PRO A . n 
+A 1 6  SER 6  6  6  SER SER A . n 
+A 1 7  ILE 7  7  7  ILE ILE A . n 
+A 1 8  VAL 8  8  8  VAL VAL A . n 
+A 1 9  ALA 9  9  9  ALA ALA A . n 
+A 1 10 ARG 10 10 10 ARG ARG A . n 
+A 1 11 SER 11 11 11 SER SER A . n 
+A 1 12 ASN 12 12 12 ASN ASN A . n 
+A 1 13 PHE 13 13 13 PHE PHE A . n 
+A 1 14 ASN 14 14 14 ASN ASN A . n 
+A 1 15 VAL 15 15 15 VAL VAL A . n 
+A 1 16 CYS 16 16 16 CYS CYS A . n 
+A 1 17 ARG 17 17 17 ARG ARG A . n 
+A 1 18 LEU 18 18 18 LEU LEU A . n 
+A 1 19 PRO 19 19 19 PRO PRO A . n 
+A 1 20 GLY 20 20 20 GLY GLY A . n 
+A 1 21 THR 21 21 21 THR THR A . n 
+A 1 22 PRO 22 22 22 PRO PRO A . n 
+A 1 23 GLU 23 23 23 GLU GLU A . n 
+A 1 24 ALA 24 24 24 ALA ALA A . n 
+A 1 25 ILE 25 25 25 ILE ILE A . n 
+A 1 26 CYS 26 26 26 CYS CYS A . n 
+A 1 27 ALA 27 27 27 ALA ALA A . n 
+A 1 28 THR 28 28 28 THR THR A . n 
+A 1 29 TYR 29 29 29 TYR TYR A . n 
+A 1 30 THR 30 30 30 THR THR A . n 
+A 1 31 GLY 31 31 31 GLY GLY A . n 
+A 1 32 CYS 32 32 32 CYS CYS A . n 
+A 1 33 ILE 33 33 33 ILE ILE A . n 
+A 1 34 ILE 34 34 34 ILE ILE A . n 
+A 1 35 ILE 35 35 35 ILE ILE A . n 
+A 1 36 PRO 36 36 36 PRO PRO A . n 
+A 1 37 GLY 37 37 37 GLY GLY A . n 
+A 1 38 ALA 38 38 38 ALA ALA A . n 
+A 1 39 THR 39 39 39 THR THR A . n 
+A 1 40 CYS 40 40 40 CYS CYS A . n 
+A 1 41 PRO 41 41 41 PRO PRO A . n 
+A 1 42 GLY 42 42 42 GLY GLY A . n 
+A 1 43 ASP 43 43 43 ASP ASP A . n 
+A 1 44 TYR 44 44 44 TYR TYR A . n 
+A 1 45 ALA 45 45 45 ALA ALA A . n 
+A 1 46 ASN 46 46 46 ASN ASN A . n 
+# 
+_software.name             PROLSQ 
+_software.classification   refinement 
+_software.version          . 
+_software.citation_id      ? 
+_software.pdbx_ordinal     1 
+# 
+loop_
+_pdbx_version.entry_id 
+_pdbx_version.revision_date 
+_pdbx_version.major_version 
+_pdbx_version.minor_version 
+_pdbx_version.revision_type 
+_pdbx_version.details 
+1CRN 2008-03-24 3 2    'Version format compliance' 'compliance with PDB format V.3.15'          
+1CRN 2011-07-13 4 0000 'Version format compliance' 'compliance with PDB Exchange Dictionary V4' 
+1CRN 2012-07-11 4 0001 Other                       'Correct SCALE records, updated Header'      
+# 
+_pdbx_struct_assembly.id                   1 
+_pdbx_struct_assembly.details              author_defined_assembly 
+_pdbx_struct_assembly.method_details       ? 
+_pdbx_struct_assembly.oligomeric_details   monomeric 
+_pdbx_struct_assembly.oligomeric_count     1 
+# 
+_pdbx_struct_assembly_gen.assembly_id       1 
+_pdbx_struct_assembly_gen.oper_expression   1 
+_pdbx_struct_assembly_gen.asym_id_list      A 
+# 
+_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 
+# 
+_pdbx_entry_details.entry_id             1CRN 
+_pdbx_entry_details.compound_details     
+;THE SECONDARY STRUCTURE SPECIFICATIONS ARE THOSE DEFINED
+IN REFERENCE 1 ABOVE AND DEPEND ON PARTICULAR DEFINITIONS
+THAT MAY AFFECT THE DETERMINATION OF END POINTS.  PLEASE
+CONSULT THE PRIMARY REFERENCE AND EXAMINE STRUCTURAL
+DETAILS SUCH AS HYDROGEN BONDING AND CONFORMATION ANGLES
+WHEN MAKING USE OF THE SPECIFICATIONS.
+;
+_pdbx_entry_details.source_details       ? 
+_pdbx_entry_details.nonpolymer_details   ? 
+_pdbx_entry_details.sequence_details     ? 
+# 
+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_deviation 
+1 1 NE A ARG 10 ? ? CZ A ARG 10 ? ? NH2 A ARG 10 ? ? -3.6 
+2 1 CB A TYR 29 ? ? CG A TYR 29 ? ? CD1 A TYR 29 ? ? -4.7 
+# 

File diff suppressed because it is too large
+ 422 - 271
package-lock.json


+ 31 - 26
package.json

@@ -12,9 +12,10 @@
   },
   "scripts": {
     "lint": "tslint src/**/*.ts",
-    "build": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ && tsc",
+    "build": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/node_modules/ && tsc",
     "watch": "tsc -watch",
-    "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html}\" build/node_modules/ --watch",
+    "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/node_modules/ --watch",
+    "watch-all-win": "start cmd /K npm run watch & start cmd /K npm run watch-extra & start cmd /K npm run watch-viewer & start http-server -p 1338",
     "test": "jest",
     "build-viewer": "webpack build/node_modules/apps/viewer/index.js --mode development -o build/viewer/index.js",
     "watch-viewer": "webpack build/node_modules/apps/viewer/index.js -w --mode development -o build/viewer/index.js",
@@ -69,44 +70,48 @@
     "@types/argparse": "^1.0.34",
     "@types/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
-    "@types/express": "^4.11.1",
-    "@types/jest": "^23.0.0",
-    "@types/node": "~10.1.2",
-    "@types/node-fetch": "^2.1.1",
-    "@types/react": "^16.3.16",
-    "@types/react-dom": "^16.0.5",
+    "@types/express": "^4.16.0",
+    "@types/jest": "^23.3.1",
+    "@types/node": "^10.5.7",
+    "@types/node-fetch": "^2.1.2",
+    "@types/react": "^16.4.8",
+    "@types/react-dom": "^16.0.7",
     "benchmark": "^2.1.4",
-    "copyfiles": "^2.0.0",
     "cpx": "^1.5.0",
-    "css-loader": "^0.28.11",
+    "css-loader": "^1.0.0",
     "extra-watch-webpack-plugin": "^1.0.3",
-    "extract-text-webpack-plugin": "^4.0.0-beta.0",
     "file-loader": "^1.1.11",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^1.0.2",
-    "jest": "^23.1.0",
+    "graphql-code-generator": "^0.10.6",
+    "graphql-codegen-typescript-template": "^0.10.6",
+    "graphql-tag": "^2.9.2",
+    "jest": "^23.4.2",
     "jest-raw-loader": "^1.0.1",
-    "node-sass": "^4.9.0",
+    "mini-css-extract-plugin": "^0.4.1",
+    "node-sass": "^4.9.2",
     "raw-loader": "^0.5.1",
     "resolve-url-loader": "^2.3.0",
-    "sass-loader": "^7.0.2",
-    "style-loader": "^0.21.0",
-    "ts-jest": "^22.4.6",
-    "tslint": "^5.10.0",
-    "typescript": "^2.9.1",
-    "uglify-js": "^3.4.0",
+    "sass-loader": "^7.1.0",
+    "style-loader": "^0.22.0",
+    "ts-jest": "^23.1.3",
+    "tslint": "^5.11.0",
+    "typescript": "^3.0.1",
+    "uglify-js": "^3.4.6",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.10.2",
-    "webpack-cli": "^3.0.1"
+    "webpack": "^4.16.5",
+    "webpack-cli": "^3.1.0"
   },
   "dependencies": {
     "argparse": "^1.0.10",
-    "compression": "^1.7.2",
+    "compression": "^1.7.3",
     "express": "^4.16.3",
+    "graphql": "^0.13.2",
+    "graphql-request": "^1.8.1",
     "immutable": "^4.0.0-rc.9",
-    "node-fetch": "^2.1.2",
-    "react": "^16.4.0",
-    "react-dom": "^16.4.0",
-    "rxjs": "^6.2.0"
+    "node-fetch": "^2.2.0",
+    "react": "^16.4.2",
+    "react-dom": "^16.4.2",
+    "rxjs": "^6.2.2"
   }
 }

+ 46 - 21
src/apps/cif2bcif/converter.ts

@@ -8,36 +8,61 @@ import CIF, { CifCategory } from 'mol-io/reader/cif'
 import { CifWriter } from 'mol-io/writer/cif'
 import * as fs from 'fs'
 import classify from './field-classifier'
+import { Progress, Task, RuntimeContext } from 'mol-task';
 
-async function getCIF(path: string) {
+function showProgress(p: Progress) {
+    process.stdout.write(`\r${new Array(80).join(' ')}`);
+    process.stdout.write(`\r${Progress.format(p)}`);
+}
+
+async function getCIF(ctx: RuntimeContext, path: string) {
     const str = fs.readFileSync(path, 'utf8');
-    const parsed = await CIF.parseText(str).run();
+    const parsed = await CIF.parseText(str).runInContext(ctx);
     if (parsed.isError) {
         throw new Error(parsed.toString());
     }
     return parsed.result;
 }
 
-function getCategoryInstanceProvider(cat: CifCategory): CifWriter.Category.Provider {
-    return function (ctx: any) {
-        return {
-            data: cat,
-            name: cat.name,
-            fields: cat.fieldNames.map(n => classify(n, cat.getField(n)!)),
-            rowCount: cat.rowCount
-        };
-    }
+function getCategoryInstanceProvider(cat: CifCategory, fields: CifWriter.Field[]): CifWriter.Category {
+    return {
+        name: cat.name,
+        instance: () => ({ data: cat, fields, rowCount: cat.rowCount })
+    };
 }
 
-export default async function convert(path: string, asText = false) {
-    const cif = await getCIF(path);
+export default function convert(path: string, asText = false) {
+    return Task.create<Uint8Array>('BinaryCIF', async ctx => {
+        const cif = await getCIF(ctx, path);
 
-    const encoder = CifWriter.createEncoder({ binary: !asText, encoderName: 'mol* cif2bcif' });
-    for (const b of cif.blocks) {
-        encoder.startDataBlock(b.header);
-        for (const c of b.categoryNames) {
-            encoder.writeCategory(getCategoryInstanceProvider(b.categories[c]));
+        const encoder = CifWriter.createEncoder({ binary: !asText, encoderName: 'mol* cif2bcif' });
+
+        let maxProgress = 0;
+        for (const b of cif.blocks) {
+            maxProgress += b.categoryNames.length;
+            for (const c of b.categoryNames) maxProgress += b.categories[c].fieldNames.length;
         }
-    }
-    return encoder.getData();
-}
+
+        let current = 0;
+        for (const b of cif.blocks) {
+            encoder.startDataBlock(b.header);
+            for (const c of b.categoryNames) {
+                const cat = b.categories[c];
+                const fields: CifWriter.Field[] = [];
+                for (const f of cat.fieldNames) {
+                    fields.push(classify(f, cat.getField(f)!))
+                    current++;
+                    if (ctx.shouldUpdate) await ctx.update({ message: 'Encoding...', current, max: maxProgress });
+                }
+
+                encoder.writeCategory(getCategoryInstanceProvider(b.categories[c], fields));
+                current++;
+                if (ctx.shouldUpdate) await ctx.update({ message: 'Encoding...', current, max: maxProgress });
+            }
+        }
+        await ctx.update('Exporting...');
+        const ret = encoder.getData() as Uint8Array;
+        await ctx.update('Done.');
+        return ret;
+    }).run(showProgress, 250);
+}

+ 174 - 3
src/apps/cif2bcif/field-classifier.ts

@@ -7,9 +7,175 @@
 import { Column } from 'mol-data/db'
 import { CifField } from 'mol-io/reader/cif/data-model'
 import { CifWriter } from 'mol-io/writer/cif'
+import { ArrayEncoder, ArrayEncoding as E } from 'mol-io/common/binary-cif';
+
+namespace IntClassifier {
+    function packSize(value: number, upperLimit: number) {
+        return value >= 0
+            ? Math.ceil((value + 1) / upperLimit)
+            : Math.ceil((value + 1) / (-upperLimit - 1));
+    }
+
+    type IntColumnInfo = { signed: boolean, limit8: number, limit16: number };
+
+    function getInfo(data: number[]): IntColumnInfo {
+        let signed = false;
+        for (let i = 0, n = data.length; i < n; i++) {
+            if (data[i] < 0) {
+                signed = true;
+                break;
+            }
+        }
+        return signed ? { signed, limit8: 0x7F, limit16: 0x7FFF } : { signed, limit8: 0xFF, limit16: 0xFFFF };
+    }
+
+    type SizeInfo = { pack8: number, pack16: number, count: number }
+    function SizeInfo(): SizeInfo { return { pack8: 0, pack16: 0, count: 0 } };
+
+    function incSize({ limit8, limit16 }: IntColumnInfo, info: SizeInfo, value: number) {
+        info.pack8 += packSize(value, limit8);
+        info.pack16 += packSize(value, limit16);
+        info.count += 1;
+    }
+
+    function incSizeSigned(info: SizeInfo, value: number) {
+        info.pack8 += packSize(value, 0x7F);
+        info.pack16 += packSize(value, 0x7FFF);
+        info.count += 1;
+    }
+
+    function byteSize(info: SizeInfo) {
+        if (info.count * 4 < info.pack16 * 2) return { length: info.count * 4, elem: 4 };
+        if (info.pack16 * 2 < info.pack8) return { length: info.pack16 * 2, elem: 2 };
+        return { length: info.pack8, elem: 1 };
+    }
+
+    function packingSize(data: number[], info: IntColumnInfo) {
+        const size = SizeInfo();
+        for (let i = 0, n = data.length; i < n; i++) {
+            incSize(info, size, data[i]);
+        }
+        return { ...byteSize(size), kind: 'pack' };
+    }
+
+    function deltaSize(data: number[], info: IntColumnInfo) {
+        const size = SizeInfo();
+        let prev = data[0];
+        for (let i = 1, n = data.length; i < n; i++) {
+            incSizeSigned(size, data[i] - prev);
+            prev = data[i];
+        }
+        return { ...byteSize(size), kind: 'delta' };
+    }
+
+    function rleSize(data: number[], info: IntColumnInfo) {
+        const size = SizeInfo();
+        let run = 1;
+        for (let i = 1, n = data.length; i < n; i++) {
+            if (data[i - 1] !== data[i]) {
+                incSize(info, size, data[i - 1]);
+                incSize(info, size, run);
+                run = 1;
+            } else {
+                run++;
+            }
+        }
+        incSize(info, size, data[data.length - 1]);
+        incSize(info, size, run);
+
+        return { ...byteSize(size), kind: 'rle' };
+    }
+
+    function deltaRleSize(data: number[], info: IntColumnInfo) {
+        const size = SizeInfo();
+        let run = 1, prev = 0, prevValue = 0;
+        for (let i = 1, n = data.length; i < n; i++) {
+            const v = data[i] - prev;
+            if (prevValue !== v) {
+                incSizeSigned(size, prevValue);
+                incSizeSigned(size, run);
+                run = 1;
+            } else {
+                run++;
+            }
+            prevValue = v;
+            prev = data[i];
+        }
+        incSizeSigned(size, prevValue);
+        incSizeSigned(size, run);
+
+        return { ...byteSize(size), kind: 'delta-rle' };
+    }
+
+    export function getSize(data: number[]) {
+        const info = getInfo(data);
+        const sizes = [packingSize(data, info), rleSize(data, info), deltaSize(data, info), deltaRleSize(data, info)];
+        sizes.sort((a, b) => a.length - b.length);
+        return sizes;
+    }
+
+    export function classify(data: number[], name: string): ArrayEncoder {
+        if (data.length < 2) return E.by(E.byteArray);
+
+        const sizes = getSize(data);
+        const size = sizes[0];
+        // console.log(`${name}: ${size.kind} ${size.length}b ${data.length}`);
+        // console.log(`${name}: ${sizes.map(s => `${s.kind}: ${s.length}b`).join(' | ')}`);
+
+        switch (size.kind) {
+            case 'pack': return E.by(E.integerPacking);
+            case 'rle': return E.by(E.runLength).and(E.integerPacking);
+            case 'delta': return E.by(E.delta).and(E.integerPacking);
+            case 'delta-rle': return E.by(E.delta).and(E.runLength).and(E.integerPacking);
+        }
+
+        throw 'bug';
+    }
+}
+
+namespace FloatClassifier {
+    const delta = 1e-6;
+    function digitCount(v: number) {
+        let m = 1;
+        for (let i = 0; i < 5; i++) {
+            const r = Math.round(m * v) / m;
+            if (Math.abs(v - r) < delta) return m;
+            m *= 10;
+        }
+        return 10000;
+    }
+
+    export function classify(data: number[], name: string) {
+        // if a vector/matrix, do not reduce precision
+        if (name.indexOf('[') > 0) return { encoder: E.by(E.byteArray), typedArray: Float64Array };
+
+        let dc = 10;
+        for (let i = 0, n = data.length; i < n; i++) dc = Math.max(dc, digitCount(data[i]));
+
+        if (dc >= 10000) return { encoder: E.by(E.byteArray), typedArray: Float64Array };
+
+        const intArray = new Int32Array(data.length);
+        for (let i = 0, n = data.length; i < n; i++) intArray[i] = data[i] * dc;
+
+        const sizes = IntClassifier.getSize(intArray as any);
+        const size = sizes[0];
+
+        // console.log(`>> ${name}: ${size.kind} ${size.length}b ${data.length} x${dc}`);
+        // console.log(`   ${name}: ${sizes.map(s => `${s.kind}: ${s.length}b`).join(' | ')}`);
+
+        switch (size.kind) {
+            case 'pack': return { encoder: E.by(E.fixedPoint(dc)).and(E.integerPacking), typedArray: Float32Array };
+            case 'rle': return { encoder: E.by(E.fixedPoint(dc)).and(E.runLength).and(E.integerPacking), typedArray: Float32Array };
+            case 'delta': return { encoder: E.by(E.fixedPoint(dc)).and(E.delta).and(E.integerPacking), typedArray: Float32Array };
+            case 'delta-rle': return { encoder: E.by(E.fixedPoint(dc)).and(E.delta).and(E.runLength).and(E.integerPacking), typedArray: Float32Array };
+        }
+
+        throw 'bug';
+    }
+}
 
 const intRegex = /^-?\d+$/
-const floatRegex = /^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?[0-9]+)?/
+const floatRegex = /^-?(([0-9]+)[.]?|([0-9]*[.][0-9]+))([(][0-9]+[)])?([eE][+-]?[0-9]+)?$/
 
 // Classify a cif field as str, int or float based the data it contains.
 // To classify a field as int or float all items are checked.
@@ -25,8 +191,13 @@ function classify(name: string, field: CifField): CifWriter.Field {
     }
 
     if (hasString) return { name, type: CifWriter.Field.Type.Str, value: field.str, valueKind: field.valueKind };
-    if (floatCount > 0) return { name, type: CifWriter.Field.Type.Float, value: field.float, valueKind: field.valueKind };
-    return { name, type: CifWriter.Field.Type.Int, value: field.int, valueKind: field.valueKind };
+    if (floatCount > 0) {
+        const { encoder, typedArray } = FloatClassifier.classify(field.toFloatArray({ array: Float64Array }) as number[], name)
+        return CifWriter.Field.float(name, field.float, { valueKind: field.valueKind, encoder, typedArray });
+    } else {
+        const encoder = IntClassifier.classify(field.toIntArray({ array: Int32Array }) as number[], name);
+        return CifWriter.Field.int(name, field.int, { valueKind: field.valueKind, encoder, typedArray: Int32Array });
+    }
 }
 
 export default classify;

+ 4 - 4
src/apps/domain-annotation-server/mapping.ts

@@ -7,7 +7,7 @@
 import { Table } from 'mol-data/db'
 import { CifWriter } from 'mol-io/writer/cif'
 import * as S from './schemas'
-import { getCategoryInstanceProvider } from './utils'
+//import { getCategoryInstanceProvider } from './utils'
 
 export default function create(allData: any) {
     const mols = Object.keys(allData);
@@ -21,7 +21,7 @@ export default function create(allData: any) {
     const sources = getSources(data);
     if (!sources._rowCount) return enc.getData();
 
-    enc.writeCategory(getCategoryInstanceProvider(`pdbx_domain_annotation_sources`, sources));
+    enc.writeCategory({ name: `pdbx_domain_annotation_sources`, instance: () => CifWriter.Category.ofTable(sources) });
 
     for (const cat of Object.keys(S.categories)) {
         writeDomain(enc, getDomain(cat, (S.categories as any)[cat], data));
@@ -38,8 +38,8 @@ type MappingRow = Table.Row<S.mapping>;
 
 function writeDomain(enc: CifWriter.Encoder, domain: DomainAnnotation | undefined) {
     if (!domain) return;
-    enc.writeCategory(getCategoryInstanceProvider(`pdbx_${domain.name}_domain_annotation`, domain.domains));
-    enc.writeCategory(getCategoryInstanceProvider(`pdbx_${domain.name}_domain_mapping`, domain.mappings));
+    enc.writeCategory({ name: `pdbx_${domain.name}_domain_annotation`, instance: () =>  CifWriter.Category.ofTable(domain.domains) });
+    enc.writeCategory({ name: `pdbx_${domain.name}_domain_mapping`, instance: () => CifWriter.Category.ofTable(domain.mappings) });
 }
 
 function getSources(data: any): Table<S.Sources> {

+ 0 - 37
src/apps/domain-annotation-server/utils.ts

@@ -1,37 +0,0 @@
-/**
- * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Table } from 'mol-data/db'
-import { CifWriter } from 'mol-io/writer/cif'
-
-function columnValue(k: string) {
-    return (i: number, d: any) => d[k].value(i);
-}
-
-function columnValueKind(k: string) {
-    return (i: number, d: any) => d[k].valueKind(i);
-}
-
-function ofSchema(schema: Table.Schema) {
-    const fields: CifWriter.Field[] = [];
-    for (const k of Object.keys(schema)) {
-        const t = schema[k];
-        const type: any = t.valueType === 'str' ? CifWriter.Field.Type.Str : t.valueType === 'int' ? CifWriter.Field.Type.Int : CifWriter.Field.Type.Float;
-        fields.push({ name: k, type, value: columnValue(k), valueKind: columnValueKind(k) })
-    }
-    return fields;
-}
-
-export function getCategoryInstanceProvider(name: string, table: Table<any>): CifWriter.Category.Provider {
-    return () => {
-        return {
-            data: table,
-            name,
-            fields: ofSchema(table._schema),
-            rowCount: table._rowCount
-        };
-    }
-}

+ 8 - 0
src/apps/schema-generator/util/cif-dic.ts

@@ -155,6 +155,7 @@ const COMMA_SEPARATED_LIST_FIELDS = [
     '_em_diffraction.tilt_angle_list', // 20,40,50,55
     '_em_entity_assembly.entity_id_list',
     '_entity.pdbx_ec',
+    '_entity_poly.pdbx_strand_id', // A,B
     '_pdbx_depui_entry_details.experimental_methods',
     '_pdbx_depui_entry_details.requested_accession_types',
     '_pdbx_soln_scatter_model.software_list', // INSIGHT II, HOMOLOGY, DISCOVERY, BIOPOLYMER, DELPHI
@@ -178,6 +179,10 @@ const SPACE_SEPARATED_LIST_FIELDS = [
     '_pdbx_soln_scatter.data_analysis_software_list', // SCTPL5 GNOM
 ];
 
+const SEMICOLON_SEPARATED_LIST_FIELDS = [
+    '_chem_comp.pdbx_synonyms' // GLYCERIN; PROPANE-1,2,3-TRIOL
+]
+
 export function generateSchema (frames: CifFrame[]) {
     const schema: Database = {}
 
@@ -250,6 +255,9 @@ export function generateSchema (frames: CifFrame[]) {
                         } else if (SPACE_SEPARATED_LIST_FIELDS.includes(d.header)) {
                             fieldType = { 'list': [ 'str', ' ' ] };
                             console.log(`space separated: ${d.header}`)
+                        } else if (SEMICOLON_SEPARATED_LIST_FIELDS.includes(d.header)) {
+                            fieldType = { 'list': [ 'str', ';' ] };
+                            console.log(`space separated: ${d.header}`)
                         }
                     }
                     fields[itemName] = fieldType

+ 143 - 71
src/apps/structure-info/model.ts

@@ -8,51 +8,47 @@
 import * as argparse from 'argparse'
 require('util.promisify').shim();
 
-// import { Table } from 'mol-data/db'
-import CIF from 'mol-io/reader/cif'
-import { Model, Structure, Element, Unit, Queries } from 'mol-model/structure'
+import { CifFrame } from 'mol-io/reader/cif'
+import { Model, Structure, StructureElement, Unit, Format, StructureProperties, UnitRing } from 'mol-model/structure'
 // import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
-import { Table } from 'mol-data/db';
-import { mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif';
 import { openCif, downloadCif } from './helpers';
-import { BitFlags } from 'mol-util';
-import { SecondaryStructureType } from 'mol-model/structure/model/types';
+import { Vec3 } from 'mol-math/linear-algebra';
 
 
 async function downloadFromPdb(pdb: string) {
     // `https://files.rcsb.org/download/${pdb}.cif`
     const parsed = await downloadCif(`http://www.ebi.ac.uk/pdbe/static/entry/${pdb}_updated.cif`, false);
-    return CIF.schema.mmCIF(parsed.blocks[0]);
+    return parsed.blocks[0];
 }
 
 async function readPdbFile(path: string) {
     const parsed = await openCif(path);
-    return CIF.schema.mmCIF(parsed.blocks[0]);
+    return parsed.blocks[0];
 }
 
 export function atomLabel(model: Model, aI: number) {
-    const { atoms, residues, chains, residueSegments, chainSegments } = model.atomicHierarchy
+    const { atoms, residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy
     const { label_atom_id } = atoms
     const { label_comp_id, label_seq_id } = residues
     const { label_asym_id } = chains
-    const rI = residueSegments.segmentMap[aI]
-    const cI = chainSegments.segmentMap[aI]
+    const rI = residueAtomSegments.index[aI]
+    const cI = chainAtomSegments.index[aI]
     return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)} ${label_atom_id.value(aI)}`
 }
 
 export function residueLabel(model: Model, rI: number) {
-    const { residues, chains, residueSegments, chainSegments } = model.atomicHierarchy
+    const { residues, chains, residueAtomSegments, chainAtomSegments } = model.atomicHierarchy
     const { label_comp_id, label_seq_id } = residues
     const { label_asym_id } = chains
-    const cI = chainSegments.segmentMap[residueSegments.segments[rI]]
+    const cI = chainAtomSegments.index[residueAtomSegments.offsets[rI]]
     return `${label_asym_id.value(cI)} ${label_comp_id.value(rI)} ${label_seq_id.value(rI)}`
 }
 
 export function printSecStructure(model: Model) {
-    console.log('Secondary Structure\n=============');
+    console.log('\nSecondary Structure\n=============');
     const { residues } = model.atomicHierarchy;
-    const { type, key } = model.properties.secondaryStructure;
+    const { key, elements } = model.properties.secondaryStructure;
 
     const count = residues._rowCount;
     let rI = 0;
@@ -61,57 +57,95 @@ export function printSecStructure(model: Model) {
         while (rI < count && key[start] === key[rI]) rI++;
         rI--;
 
-        if (BitFlags.has(type[start], SecondaryStructureType.Flag.Beta)) {
-            console.log(`Sheet: ${residueLabel(model, start)} - ${residueLabel(model, rI)} (key ${key[start]})`);
-        } else if (BitFlags.has(type[start], SecondaryStructureType.Flag.Helix)) {
-            console.log(`Helix: ${residueLabel(model, start)} - ${residueLabel(model, rI)} (key ${key[start]})`);
-        }
-
+        const e = elements[key[start]];
+        if (e.kind !== 'none') console.log(`${e.kind}: ${residueLabel(model, start)} - ${residueLabel(model, rI)}`);
         rI++;
     }
 }
 
-export function printBonds(structure: Structure) {
-    for (const unit of structure.units) {
-        if (!Unit.isAtomic(unit)) continue;
+export function printLinks(structure: Structure, showIntra: boolean, showInter: boolean) {
+    if (showIntra) {
+        console.log('\nIntra Unit Links\n=============');
+        for (const unit of structure.units) {
+            if (!Unit.isAtomic(unit)) continue;
 
-        const elements = unit.elements;
-        const { count, offset, neighbor } = unit.bonds;
-        const { model }  = unit;
+            const elements = unit.elements;
+            const { a, b, edgeCount } = unit.links;
+            const { model } = unit;
+
+            if (!edgeCount) continue;
+
+            for (let bI = 0, _bI = edgeCount * 2; bI < _bI; bI++) {
+                const x = a[bI], y = b[bI];
+                if (x >= y) continue;
+                console.log(`${atomLabel(model, elements[x])} -- ${atomLabel(model, elements[y])}`);
+            }
+        }
+    }
 
-        if (!count) continue;
+    if (showInter) {
+        console.log('\nInter Unit Links\n=============');
+        const links = structure.links;
+        for (const unit of structure.units) {
+            if (!Unit.isAtomic(unit)) continue;
 
-        for (let j = 0; j < offset.length - 1; ++j) {
-            const start = offset[j];
-            const end = offset[j + 1];
+            for (const pairLinks of links.getLinkedUnits(unit)) {
+                if (!pairLinks.areUnitsOrdered || pairLinks.bondCount === 0) continue;
 
-            if (end <= start) continue;
+                const { unitA, unitB } = pairLinks;
+                console.log(`${pairLinks.unitA.id} - ${pairLinks.unitB.id}: ${pairLinks.bondCount} bond(s)`);
 
-            const aI = elements[j];
-            for (let _bI = start; _bI < end; _bI++) {
-                const bI = elements[neighbor[_bI]];
-                console.log(`${atomLabel(model, aI)} -- ${atomLabel(model, bI)}`);
+                for (const aI of pairLinks.linkedElementIndices) {
+                    for (const link of pairLinks.getBonds(aI)) {
+                        console.log(`${atomLabel(unitA.model, unitA.elements[aI])} -- ${atomLabel(unitB.model, unitB.elements[link.indexB])}`);
+                    }
+                }
             }
         }
     }
 }
 
 export function printSequence(model: Model) {
-    console.log('Sequence\n=============');
+    console.log('\nSequence\n=============');
     const { byEntityKey } = model.sequence;
     for (const key of Object.keys(byEntityKey)) {
         const seq = byEntityKey[+key];
-        console.log(`${seq.entityId} (${seq.num.value(0)}, ${seq.num.value(seq.num.rowCount - 1)}) (${seq.compId.value(0)}, ${seq.compId.value(seq.compId.rowCount - 1)})`);
-        // for (let i = 0; i < seq.compId.rowCount; i++) {
-        //     console.log(`${seq.entityId} ${seq.num.value(i)} ${seq.compId.value(i)}`);
-        // }
+        console.log(`${seq.entityId} (${seq.sequence.kind} ${seq.num.value(0)} (offset ${seq.sequence.offset}), ${seq.num.value(seq.num.rowCount - 1)}) (${seq.compId.value(0)}, ${seq.compId.value(seq.compId.rowCount - 1)})`);
+        console.log(`${seq.sequence.sequence}`);
+    }
+    console.log();
+}
+
+export function printModRes(model: Model) {
+    console.log('\nModified Residues\n=============');
+    const map = model.properties.modifiedResidues.parentId;
+    const { label_comp_id, _rowCount } = model.atomicHierarchy.residues;
+    for (let i = 0; i < _rowCount; i++) {
+        const comp_id = label_comp_id.value(i);
+        if (!map.has(comp_id)) continue;
+        console.log(`[${i}] ${map.get(comp_id)} -> ${comp_id}`);
+    }
+    console.log();
+}
+
+export function printRings(structure: Structure) {
+    console.log('\nRings\n=============');
+    for (const unit of structure.units) {
+        if (!Unit.isAtomic(unit)) continue;
+        const { all, byFingerprint } = unit.rings;
+        const fps: string[] = [];
+        for (let i = 0, _i = Math.min(5, all.length); i < _i; i++) {
+            fps[fps.length] = UnitRing.fingerprint(unit, all[i]);
+        }
+        if (all.length > 5) fps.push('...')
+        console.log(`Unit ${unit.id}, ${all.length} ring(s), ${byFingerprint.size} different fingerprint(s).\n  ${fps.join(', ')}`);
     }
     console.log();
 }
 
 export function printUnits(structure: Structure) {
-    console.log('Units\n=============');
-    const l = Element.Location();
+    console.log('\nUnits\n=============');
+    const l = StructureElement.create();
 
     for (const unit of structure.units) {
         l.unit = unit;
@@ -123,7 +157,7 @@ export function printUnits(structure: Structure) {
         } else if (Unit.isCoarse(l.unit)) {
             console.log(`Coarse unit ${unit.id} ${unit.conformation.operator.name} (${Unit.isSpheres(l.unit) ? 'spheres' : 'gaussians'}): ${size} elements.`);
 
-            const props = Queries.props.coarse;
+            const props = StructureProperties.coarse;
             const seq = l.unit.model.sequence;
 
             for (let j = 0, _j = Math.min(size, 3); j < _j; j++) {
@@ -140,48 +174,86 @@ export function printUnits(structure: Structure) {
     }
 }
 
+export function printSymmetryInfo(model: Model) {
+    console.log('\nSymmetry Info\n=============');
+    const { symmetry } = model;
+    const { size, anglesInRadians } = symmetry.spacegroup.cell;
+    console.log(`Spacegroup: ${symmetry.spacegroup.name} size: ${Vec3.toString(size)} angles: ${Vec3.toString(anglesInRadians)}`);
+    console.log(`Assembly names: ${symmetry.assemblies.map(a => a.id).join(', ')}`);
+    // NCS example: 1auy
+    console.log(`NCS operators: ${symmetry.ncsOperators && symmetry.ncsOperators.map(a => a.name).join(', ')}`);
+}
+
+export function printModelStats(models: ReadonlyArray<Model>) {
+    console.log('\nModels\n=============');
 
-export function printIHMModels(model: Model) {
-    if (!model.coarseHierarchy.isDefined) return false;
-    console.log('IHM Models\n=============');
-    console.log(Table.formatToString(model.coarseHierarchy.models));
+    for (const m of models) {
+        if (m.coarseHierarchy.isDefined) {
+            console.log(`${m.label} ${m.modelNum}: ${m.atomicHierarchy.atoms._rowCount} atom(s), ${m.coarseHierarchy.spheres.count} sphere(s), ${m.coarseHierarchy.gaussians.count} gaussian(s)`);
+        } else {
+            console.log(`${m.label} ${m.modelNum}: ${m.atomicHierarchy.atoms._rowCount} atom(s)`);
+        }
+    }
+    console.log();
 }
 
-async function run(mmcif: mmCIF_Database) {
-    const models = await Model.create({ kind: 'mmCIF', data: mmcif }).run();
+async function run(frame: CifFrame, args: Args) {
+    const models = await Model.create(Format.mmCIF(frame)).run();
     const structure = Structure.ofModel(models[0]);
-    printSequence(models[0]);
-    printIHMModels(models[0]);
-    printUnits(structure);
-    // printBonds(structure);
-    printSecStructure(models[0]);
+
+    if (args.models) printModelStats(models);
+    if (args.seq) printSequence(models[0]);
+    if (args.units) printUnits(structure);
+    if (args.sym) printSymmetryInfo(models[0]);
+    if (args.rings) printRings(structure);
+    if (args.intraLinks) printLinks(structure, true, false);
+    if (args.interLinks) printLinks(structure, false, true);
+    if (args.mod) printModRes(models[0]);
+    if (args.sec) printSecStructure(models[0]);
 }
 
-async function runDL(pdb: string) {
+async function runDL(pdb: string, args: Args) {
     const mmcif = await downloadFromPdb(pdb)
-    run(mmcif);
+    run(mmcif, args);
 }
 
-async function runFile(filename: string) {
+async function runFile(filename: string, args: Args) {
     const mmcif = await readPdbFile(filename);
-    run(mmcif);
+    run(mmcif, args);
 }
 
 const parser = new argparse.ArgumentParser({
-  addHelp: true,
-  description: 'Print info about a structure, mainly to test and showcase the mol-model module'
-});
-parser.addArgument([ '--download', '-d' ], {
-    help: 'Pdb entry id'
-});
-parser.addArgument([ '--file', '-f' ], {
-    help: 'filename'
+    addHelp: true,
+    description: 'Print info about a structure, mainly to test and showcase the mol-model module'
 });
+parser.addArgument(['--download', '-d'], { help: 'Pdb entry id' });
+parser.addArgument(['--file', '-f'], { help: 'filename' });
+
+parser.addArgument(['--models'], { help: 'print models info', action: 'storeTrue' });
+parser.addArgument(['--seq'], { help: 'print sequence', action: 'storeTrue' });
+parser.addArgument(['--units'], { help: 'print units', action: 'storeTrue' });
+parser.addArgument(['--sym'], { help: 'print symmetry', action: 'storeTrue' });
+parser.addArgument(['--rings'], { help: 'print rings', action: 'storeTrue' });
+parser.addArgument(['--intraLinks'], { help: 'print intra unit links', action: 'storeTrue' });
+parser.addArgument(['--interLinks'], { help: 'print inter unit links', action: 'storeTrue' });
+parser.addArgument(['--mod'], { help: 'print modified residues', action: 'storeTrue' });
+parser.addArgument(['--sec'], { help: 'print secoundary structure', action: 'storeTrue' });
 interface Args {
     download?: string,
-    file?: string
+    file?: string,
+
+    models?:boolean,
+    seq?: boolean,
+    ihm?: boolean,
+    units?: boolean,
+    sym?: boolean,
+    rings?: boolean,
+    intraLinks?: boolean,
+    interLinks?: boolean,
+    mod?: boolean,
+    sec?: boolean,
 }
 const args: Args = parser.parseArgs();
 
-if (args.download) runDL(args.download)
-else if (args.file) runFile(args.file)
+if (args.download) runDL(args.download, args)
+else if (args.file) runFile(args.file, args)

+ 25 - 0
src/apps/viewer/index.tsx

@@ -22,6 +22,10 @@ import { EntityTree } from 'mol-app/ui/entity/tree';
 import { EntityTreeController } from 'mol-app/controller/entity/tree';
 import { TransformListController } from 'mol-app/controller/transform/list';
 import { TransformList } from 'mol-app/ui/transform/list';
+import { SequenceView } from 'mol-app/ui/visualization/sequence-view';
+import { InteractivityEvents } from 'mol-app/event/basic';
+import { MarkerAction } from 'mol-geo/util/marker-data';
+import { EveryLoci } from 'mol-model/loci';
 
 const elm = document.getElementById('app')
 if (!elm) throw new Error('Can not find element with id "app".')
@@ -45,6 +49,14 @@ targets[LayoutRegion.Bottom].components.push({
     isStatic: true
 });
 
+targets[LayoutRegion.Top].components.push({
+    key: 'molstar-sequence-view',
+    controller: ctx.components.sequenceView,
+    region: LayoutRegion.Top,
+    view: SequenceView,
+    isStatic: true
+});
+
 targets[LayoutRegion.Main].components.push({
     key: 'molstar-background-jobs',
     controller: new JobsController(ctx, 'Background'),
@@ -85,4 +97,17 @@ ctx.layout.setState({
 })
 // ctx.viewport.setState()
 
+ctx.dispatcher.getStream(InteractivityEvents.HighlightLoci).subscribe(event => {
+    ctx.stage.viewer.mark(EveryLoci, MarkerAction.RemoveHighlight)
+    if (event && event.data) {
+        ctx.stage.viewer.mark(event.data, MarkerAction.Highlight)
+    }
+})
+
+ctx.dispatcher.getStream(InteractivityEvents.SelectLoci).subscribe(event => {
+    if (event && event.data) {
+        ctx.stage.viewer.mark(event.data, MarkerAction.ToggleSelect)
+    }
+})
+
 ReactDOM.render(React.createElement(Layout, { controller: ctx.layout }), elm);

+ 3 - 0
src/helpers.d.ts

@@ -13,4 +13,7 @@ declare module Helpers {
     export type NumberArray = TypedArray | number[]
     export type UintArray = Uint8Array | Uint16Array | Uint32Array | number[]
     export type ValueOf<T> = T[keyof T]
+    export type ArrayCtor<T> = { new(size: number): { [i: number]: T, length: number } }
+    /** assignable ArrayLike version */
+    export type ArrayLike<T> =  { [i: number]: T, length: number }
 }

+ 7 - 1
src/mol-app/context/context.ts

@@ -15,6 +15,7 @@ import { Stage } from 'mol-view/stage';
 import { AnyTransform } from 'mol-view/state/transform';
 import { BehaviorSubject } from 'rxjs';
 import { AnyEntity } from 'mol-view/state/entity';
+import { SequenceViewController } from '../controller/visualization/sequence-view';
 
 export class Settings {
     private settings = new Map<string, any>();
@@ -35,11 +36,16 @@ export class Context {
     logger = new Logger(this);
     performance = new PerformanceMonitor();
 
-    stage = new Stage();
+    stage = new Stage(this);
     viewport = new ViewportController(this);
     layout: LayoutController;
     settings = new Settings();
 
+    // TODO: this is a temporary solution
+    components = {
+        sequenceView: new SequenceViewController(this)
+    };
+
     currentEntity = new BehaviorSubject(undefined) as BehaviorSubject<AnyEntity | undefined>
     currentTransforms = new BehaviorSubject([] as AnyTransform[])
 

+ 21 - 0
src/mol-app/controller/visualization/sequence-view.ts

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { shallowClone } from 'mol-util';
+import { Context } from '../../context/context'
+import { Controller } from '../controller';
+import { Structure } from 'mol-model/structure';
+
+export const DefaultSequenceViewState = {
+    structure: void 0 as (Structure | undefined)
+}
+export type SequenceViewState = typeof DefaultSequenceViewState
+
+export class SequenceViewController extends Controller<SequenceViewState> {
+    constructor(context: Context) {
+        super(context, shallowClone(DefaultSequenceViewState));
+    }
+}

+ 7 - 0
src/mol-app/event/basic.ts

@@ -11,6 +11,7 @@ import { Dispatcher } from '../service/dispatcher'
 import { LayoutState } from '../controller/layout';
 import { ViewportOptions } from '../controller/visualization/viewport';
 import { Job } from '../service/job';
+import { Loci } from 'mol-model/loci';
 
 const Lane = Dispatcher.Lane;
 
@@ -31,3 +32,9 @@ export namespace LayoutEvents {
     export const SetState = Event.create<Partial<LayoutState>>('lm.cmd.Layout.SetState', Lane.Slow);
     export const SetViewportOptions = Event.create<ViewportOptions>('bs.cmd.Layout.SetViewportOptions', Lane.Slow);
 }
+
+export namespace InteractivityEvents {
+    export const HighlightLoci = Event.create<Loci>('bs.Interactivity.HighlightLoci', Lane.Slow);
+    export const SelectLoci = Event.create<Loci>('bs.Interactivity.SelectLoci', Lane.Slow);
+    export const LabelLoci = Event.create<Loci>('bs.Interactivity.LabelLoci', Lane.Slow);
+}

+ 9 - 0
src/mol-app/skin/components/sequence-view.scss

@@ -0,0 +1,9 @@
+.molstar-sequence-view-wrap {
+    position: absolute;
+    right: 0;
+    top: 0;
+    left: 0;
+    bottom: 0;
+    overflow: hidden;
+    overflow-x: scroll;
+}

+ 1 - 1
src/mol-app/skin/layout/common.scss

@@ -23,7 +23,7 @@
     overflow: hidden;
 }
 
-.molstar-layout-main, .molstar-layout-bottom {
+.molstar-layout-main, .molstar-layout-bottom, .molstar-layout-top {
     .molstar-layout-static {
         left: 0;
         right: 0;

+ 2 - 1
src/mol-app/skin/ui.scss

@@ -35,4 +35,5 @@
 @import 'components/misc';
 @import 'components/panel';
 @import 'components/slider';
-@import 'components/viewport';
+@import 'components/viewport';
+@import 'components/sequence-view';

+ 6 - 0
src/mol-app/ui/controls/slider.tsx

@@ -206,6 +206,12 @@ export class SliderBase extends React.Component<SliderBaseProps, SliderBaseState
     private sliderElement: HTMLElement | undefined = void 0;
     private handleElements: (HTMLElement | undefined)[] = [];
 
+    state: SliderBaseState = {
+        handle: null,
+        recent: 0,
+        bounds: [0, 0],
+    };
+
     constructor(props: SliderBaseProps) {
         super(props);
 

+ 15 - 3
src/mol-app/ui/entity/tree.tsx

@@ -13,13 +13,13 @@ import { View } from '../view';
 import { EntityTreeController } from '../../controller/entity/tree';
 import { Controller } from '../../controller/controller';
 import { AnyEntity, RootEntity } from 'mol-view/state/entity';
-import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill, StructureCenter } from 'mol-view/state/transform';
+import { AnyTransform, SpacefillUpdate, UrlToData, DataToCif, FileToData, CifToMmcif, MmcifToModel, ModelToStructure, StructureToSpacefill, MmcifFileToSpacefill, StructureCenter, StructureToBallAndStick, DistanceRestraintUpdate, CartoonUpdate, BallAndStickUpdate, BackboneUpdate, MmcifUrlToSpacefill } from 'mol-view/state/transform';
 
 function getTransforms(entity: AnyEntity): AnyTransform[] {
     const transforms: AnyTransform[] = []
     switch (entity.kind) {
         case 'root':
-            transforms.push(MmcifFileToSpacefill)
+            transforms.push(MmcifFileToSpacefill, MmcifUrlToSpacefill)
             break;
         case 'url':
             transforms.push(UrlToData)
@@ -40,11 +40,23 @@ function getTransforms(entity: AnyEntity): AnyTransform[] {
             transforms.push(ModelToStructure)
             break;
         case 'structure':
-            transforms.push(StructureToSpacefill, StructureCenter)
+            transforms.push(StructureToSpacefill, StructureToBallAndStick, StructureCenter)
             break;
         case 'spacefill':
             transforms.push(SpacefillUpdate)
             break;
+        case 'ballandstick':
+            transforms.push(BallAndStickUpdate)
+            break;
+        case 'distancerestraint':
+            transforms.push(DistanceRestraintUpdate)
+            break;
+        case 'backbone':
+            transforms.push(BackboneUpdate)
+            break;
+        case 'cartoon':
+            transforms.push(CartoonUpdate)
+            break;
     }
     return transforms
 }

+ 243 - 0
src/mol-app/ui/transform/backbone.tsx

@@ -0,0 +1,243 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { Toggle } from '../controls/common';
+import { BackboneEntity } from 'mol-view/state/entity';
+import { BackboneUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface BackboneState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    detail: number
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
+}
+
+export class Backbone extends View<Controller<any>, BackboneState, { transform: BackboneUpdate, entity: BackboneEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<BackboneState>) {
+        console.log(state)
+        const { transform, entity, ctx } = this.props
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform } = this.props
+
+        const qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
+            return <option key={value} value={value}>{value.toString()}</option>
+        })
+
+        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorValueOptions = Object.keys(ColorNames).map((name, idx) => {
+            return <option key={name} value={(ColorNames as any)[name]}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.update()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                    <div className='molstar-control-row molstar-options-group'>
+                        <span>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Sphere detail</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.detail}
+                                    onChange={(e) => this.update({ detail: parseInt(e.target.value) })}
+                                >
+                                    {sphereDetailOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color theme</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorTheme.name}
+                                    onChange={(e) => {
+                                        const colorThemeName = e.target.value as ColorThemeInfo
+                                        if (colorThemeName === 'uniform') {
+                                            this.update({
+                                                colorTheme: {
+                                                    name: colorThemeName,
+                                                    value: this.state.colorValue
+                                                }
+                                            })
+                                        } else {
+                                            this.update({
+                                                colorTheme: { name: colorThemeName }
+                                            })
+                                        }
+                                    }}
+                                >
+                                    {colorThemeOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color value</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorValue}
+                                    onChange={(e) => {
+                                        const colorValue = parseInt(e.target.value)
+                                        this.update({
+                                            colorTheme: {
+                                                name: 'uniform',
+                                                value: colorValue
+                                            },
+                                            colorValue
+                                        })
+                                    }}
+                                >
+                                    {colorValueOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.visible}
+                                    label='Visibility'
+                                    onChange={value => this.update({ visible: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.depthMask}
+                                    label='Depth write'
+                                    onChange={value => this.update({ depthMask: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.doubleSided}
+                                    label='Double sided'
+                                    onChange={value => this.update({ doubleSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flipSided}
+                                    label='Flip sided'
+                                    onChange={value => this.update({ flipSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flatShaded}
+                                    label='Flat shaded'
+                                    onChange={value => this.update({ flatShaded: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.alpha}
+                                    label='Opacity'
+                                    min={0}
+                                    max={1}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({ alpha: value })}
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 234 - 0
src/mol-app/ui/transform/ball-and-stick.tsx

@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { Toggle } from '../controls/common';
+import { DistanceRestraintEntity } from 'mol-view/state/entity';
+import { DistanceRestraintUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface BallAndStickState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    linkScale: number
+    linkSpacing: number
+    linkRadius: number
+    radialSegments: number
+    detail: number
+    unitKinds: Unit.Kind[]
+}
+
+export class BallAndStick extends View<Controller<any>, BallAndStickState, { transform: DistanceRestraintUpdate, entity: DistanceRestraintEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        linkScale: 0.4,
+        linkSpacing: 1,
+        linkRadius: 0.25,
+        radialSegments: 16,
+        detail: 1,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<BallAndStickState>) {
+        const { transform, entity, ctx } = this.props
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform } = this.props
+
+        const qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorValueOptions = Object.keys(ColorNames).map((name, idx) => {
+            return <option key={name} value={(ColorNames as any)[name]}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.update()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color theme</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorTheme.name}
+                                    onChange={(e) => {
+                                        const colorThemeName = e.target.value as ColorThemeInfo
+                                        if (colorThemeName === 'uniform') {
+                                            this.update({
+                                                colorTheme: {
+                                                    name: colorThemeName,
+                                                    value: this.state.colorValue
+                                                }
+                                            })
+                                        } else {
+                                            this.update({
+                                                colorTheme: { name: colorThemeName }
+                                            })
+                                        }
+                                    }}
+                                >
+                                    {colorThemeOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color value</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorValue}
+                                    onChange={(e) => {
+                                        const colorValue = parseInt(e.target.value)
+                                        this.update({
+                                            colorTheme: {
+                                                name: 'uniform',
+                                                value: colorValue
+                                            },
+                                            colorValue
+                                        })
+                                    }}
+                                >
+                                    {colorValueOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.visible}
+                                    label='Visibility'
+                                    onChange={value => this.update({ visible: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.depthMask}
+                                    label='Depth write'
+                                    onChange={value => this.update({ depthMask: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.doubleSided}
+                                    label='Double sided'
+                                    onChange={value => this.update({ doubleSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flipSided}
+                                    label='Flip sided'
+                                    onChange={value => this.update({ flipSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flatShaded}
+                                    label='Flat shaded'
+                                    onChange={value => this.update({ flatShaded: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.alpha}
+                                    label='Opacity'
+                                    min={0}
+                                    max={1}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({ alpha: value })}
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 243 - 0
src/mol-app/ui/transform/cartoon.tsx

@@ -0,0 +1,243 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { Toggle } from '../controls/common';
+import { CartoonEntity } from 'mol-view/state/entity';
+import { CartoonUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface CartoonState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    detail: number
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
+}
+
+export class Cartoon extends View<Controller<any>, CartoonState, { transform: CartoonUpdate, entity: CartoonEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        detail: 2,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<CartoonState>) {
+        console.log(state)
+        const { transform, entity, ctx } = this.props
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform } = this.props
+
+        const qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
+            return <option key={value} value={value}>{value.toString()}</option>
+        })
+
+        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorValueOptions = Object.keys(ColorNames).map((name, idx) => {
+            return <option key={name} value={(ColorNames as any)[name]}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.update()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                    <div className='molstar-control-row molstar-options-group'>
+                        <span>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Sphere detail</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.detail}
+                                    onChange={(e) => this.update({ detail: parseInt(e.target.value) })}
+                                >
+                                    {sphereDetailOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color theme</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorTheme.name}
+                                    onChange={(e) => {
+                                        const colorThemeName = e.target.value as ColorThemeInfo
+                                        if (colorThemeName === 'uniform') {
+                                            this.update({
+                                                colorTheme: {
+                                                    name: colorThemeName,
+                                                    value: this.state.colorValue
+                                                }
+                                            })
+                                        } else {
+                                            this.update({
+                                                colorTheme: { name: colorThemeName }
+                                            })
+                                        }
+                                    }}
+                                >
+                                    {colorThemeOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color value</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorValue}
+                                    onChange={(e) => {
+                                        const colorValue = parseInt(e.target.value)
+                                        this.update({
+                                            colorTheme: {
+                                                name: 'uniform',
+                                                value: colorValue
+                                            },
+                                            colorValue
+                                        })
+                                    }}
+                                >
+                                    {colorValueOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.visible}
+                                    label='Visibility'
+                                    onChange={value => this.update({ visible: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.depthMask}
+                                    label='Depth write'
+                                    onChange={value => this.update({ depthMask: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.doubleSided}
+                                    label='Double sided'
+                                    onChange={value => this.update({ doubleSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flipSided}
+                                    label='Flip sided'
+                                    onChange={value => this.update({ flipSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flatShaded}
+                                    label='Flat shaded'
+                                    onChange={value => this.update({ flatShaded: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.alpha}
+                                    label='Opacity'
+                                    min={0}
+                                    max={1}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({ alpha: value })}
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 234 - 0
src/mol-app/ui/transform/distance-restraint.tsx

@@ -0,0 +1,234 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Adapted from LiteMol
+ * Copyright (c) 2016 - now David Sehnal, licensed under Apache 2.0, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+
+import { View } from '../view';
+import { Controller } from '../../controller/controller';
+import { Toggle } from '../controls/common';
+import { DistanceRestraintEntity } from 'mol-view/state/entity';
+import { DistanceRestraintUpdate } from 'mol-view/state/transform'
+import { StateContext } from 'mol-view/state/context';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
+import { Color, ColorNames } from 'mol-util/color';
+import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
+
+export const ColorThemeInfo = {
+    'atom-index': {},
+    'chain-id': {},
+    'element-symbol': {},
+    'instance-index': {},
+    'uniform': {}
+}
+export type ColorThemeInfo = keyof typeof ColorThemeInfo
+
+interface DistanceRestraintState {
+    doubleSided: boolean
+    flipSided: boolean
+    flatShaded: boolean
+    colorTheme: ColorTheme
+    colorValue: Color
+    sizeTheme: SizeTheme
+    visible: boolean
+    alpha: number
+    depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    linkScale: number
+    linkSpacing: number
+    linkRadius: number
+    radialSegments: number
+    detail: number
+    unitKinds: Unit.Kind[]
+}
+
+export class DistanceRestraint extends View<Controller<any>, DistanceRestraintState, { transform: DistanceRestraintUpdate, entity: DistanceRestraintEntity, ctx: StateContext }> {
+    state = {
+        doubleSided: true,
+        flipSided: false,
+        flatShaded: false,
+        colorTheme: { name: 'element-symbol' } as ColorTheme,
+        colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
+        visible: true,
+        alpha: 1,
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        linkScale: 0.4,
+        linkSpacing: 1,
+        linkRadius: 0.25,
+        radialSegments: 16,
+        detail: 1,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
+    }
+
+    update(state?: Partial<DistanceRestraintState>) {
+        const { transform, entity, ctx } = this.props
+        const newState = { ...this.state, ...state }
+        this.setState(newState)
+        transform.apply(ctx, entity, newState)
+    }
+
+    render() {
+        const { transform } = this.props
+
+        const qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorThemeOptions = Object.keys(ColorThemeInfo).map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        const colorValueOptions = Object.keys(ColorNames).map((name, idx) => {
+            return <option key={name} value={(ColorNames as any)[name]}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-header'>
+                    <button
+                        className='molstar-btn molstar-btn-link molstar-panel-expander'
+                        onClick={() => this.update()}
+                    >
+                        <span>[{transform.kind}] {transform.inputKind} -> {transform.outputKind}</span>
+                    </button>
+                </div>
+                <div className='molstar-panel-body'>
+                    <div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color theme</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorTheme.name}
+                                    onChange={(e) => {
+                                        const colorThemeName = e.target.value as ColorThemeInfo
+                                        if (colorThemeName === 'uniform') {
+                                            this.update({
+                                                colorTheme: {
+                                                    name: colorThemeName,
+                                                    value: this.state.colorValue
+                                                }
+                                            })
+                                        } else {
+                                            this.update({
+                                                colorTheme: { name: colorThemeName }
+                                            })
+                                        }
+                                    }}
+                                >
+                                    {colorThemeOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Color value</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.colorValue}
+                                    onChange={(e) => {
+                                        const colorValue = parseInt(e.target.value)
+                                        this.update({
+                                            colorTheme: {
+                                                name: 'uniform',
+                                                value: colorValue
+                                            },
+                                            colorValue
+                                        })
+                                    }}
+                                >
+                                    {colorValueOptions}
+                                </select>
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.visible}
+                                    label='Visibility'
+                                    onChange={value => this.update({ visible: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.depthMask}
+                                    label='Depth write'
+                                    onChange={value => this.update({ depthMask: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.doubleSided}
+                                    label='Double sided'
+                                    onChange={value => this.update({ doubleSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flipSided}
+                                    label='Flip sided'
+                                    onChange={value => this.update({ flipSided: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Toggle
+                                    value={this.state.flatShaded}
+                                    label='Flat shaded'
+                                    onChange={value => this.update({ flatShaded: value })}
+                                />
+                            </div>
+                        </div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <div>
+                                <Slider
+                                    value={this.state.alpha}
+                                    label='Opacity'
+                                    min={0}
+                                    max={1}
+                                    step={0.01}
+                                    callOnChangeWhileSliding={true}
+                                    onChange={value => this.update({ alpha: value })}
+                                />
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 46 - 4
src/mol-app/ui/transform/file-loader.tsx

@@ -9,18 +9,60 @@ import { View } from '../view';
 import { FileInput } from '../controls/common';
 import { TransformListController } from '../../controller/transform/list';
 import { FileEntity } from 'mol-view/state/entity';
-import { MmcifFileToSpacefill } from 'mol-view/state/transform';
+import { MmcifFileToModel, ModelToStructure, StructureToBallAndStick, StructureToSpacefill, StructureToDistanceRestraint, StructureToBackbone } from 'mol-view/state/transform';
 import { StateContext } from 'mol-view/state/context';
+import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
+import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-stick';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/backbone';
+
+const spacefillProps: SpacefillProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
+const ballAndStickProps: BallAndStickProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    sizeTheme: { name: 'uniform', value: 0.05 },
+    linkRadius: 0.05,
+    quality: 'auto',
+    useFog: false
+}
+
+const distanceRestraintProps: DistanceRestraintProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    linkRadius: 0.5,
+    quality: 'auto',
+    useFog: false
+}
+
+const backboneProps: BackboneProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
 
 export class FileLoader extends View<TransformListController, {}, { ctx: StateContext }> {
     render() {
         return <div className='molstar-file-loader'>
             <FileInput
                 accept='*.cif'
-                onChange={files => {
+                onChange={async files => {
                     if (files) {
-                        const fileEntity = FileEntity.ofFile(this.props.ctx, files[0])
-                        MmcifFileToSpacefill.apply(this.props.ctx, fileEntity)
+                        const ctx = this.props.ctx
+                        const fileEntity = FileEntity.ofFile(ctx, files[0])
+                        const modelEntity = await MmcifFileToModel.apply(ctx, fileEntity)
+                        const structureEntity = await ModelToStructure.apply(ctx, modelEntity)
+
+                        StructureToBallAndStick.apply(ctx, structureEntity, { ...ballAndStickProps, visible: true })
+                        StructureToSpacefill.apply(ctx, structureEntity, { ...spacefillProps, visible: false })
+                        StructureToDistanceRestraint.apply(ctx, structureEntity, { ...distanceRestraintProps, visible: false })
+                        StructureToBackbone.apply(ctx, structureEntity, { ...backboneProps, visible: true })
                     }
                 }}
             />

+ 15 - 0
src/mol-app/ui/transform/list.tsx

@@ -14,13 +14,20 @@ import { Controller } from '../../controller/controller';
 import { TransformListController } from '../../controller/transform/list';
 import { AnyTransform } from 'mol-view/state/transform';
 import { Spacefill } from './spacefill';
+import { BallAndStick } from './ball-and-stick';
 import { AnyEntity } from 'mol-view/state/entity';
 import { FileLoader } from './file-loader';
 import { ModelToStructure } from './model';
 import { StructureCenter } from './structure';
+import { Cartoon } from './cartoon';
+import { DistanceRestraint } from './distance-restraint';
+import { Backbone } from './backbone';
+import { UrlLoader } from './url-loader';
 
 function getTransformComponent(controller: TransformListController, entity: AnyEntity, transform: AnyTransform) {
     switch (transform.kind) {
+        case 'url-to-spacefill':
+            return <UrlLoader controller={controller} ctx={controller.context.stage.ctx}></UrlLoader>
         case 'file-to-spacefill':
             return <FileLoader controller={controller} ctx={controller.context.stage.ctx}></FileLoader>
         case 'model-to-structure':
@@ -29,6 +36,14 @@ function getTransformComponent(controller: TransformListController, entity: AnyE
             return <StructureCenter controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></StructureCenter>
         case 'spacefill-update':
             return <Spacefill controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Spacefill>
+        case 'ballandstick-update':
+            return <BallAndStick controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></BallAndStick>
+        case 'distancerestraint-update':
+            return <DistanceRestraint controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></DistanceRestraint>
+        case 'backbone-update':
+            return <Backbone controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Backbone>
+        case 'cartoon-update':
+            return <Cartoon controller={controller} entity={entity} transform={transform} ctx={controller.context.stage.ctx}></Cartoon>
     }
     return <Transform controller={controller} entity={entity} transform={transform}></Transform>
 }

+ 33 - 2
src/mol-app/ui/transform/spacefill.tsx

@@ -15,9 +15,11 @@ import { Toggle } from '../controls/common';
 import { SpacefillEntity } from 'mol-view/state/entity';
 import { SpacefillUpdate } from 'mol-view/state/transform'
 import { StateContext } from 'mol-view/state/context';
-import { ColorTheme } from 'mol-geo/theme';
+import { ColorTheme, SizeTheme } from 'mol-geo/theme';
 import { Color, ColorNames } from 'mol-util/color';
 import { Slider } from '../controls/slider';
+import { VisualQuality } from 'mol-geo/representation/util';
+import { Unit } from 'mol-model/structure';
 
 export const ColorThemeInfo = {
     'atom-index': {},
@@ -35,9 +37,13 @@ interface SpacefillState {
     detail: number
     colorTheme: ColorTheme
     colorValue: Color
+    sizeTheme: SizeTheme
     visible: boolean
     alpha: number
     depthMask: boolean
+    useFog: boolean
+    quality: VisualQuality
+    unitKinds: Unit.Kind[]
 }
 
 export class Spacefill extends View<Controller<any>, SpacefillState, { transform: SpacefillUpdate, entity: SpacefillEntity, ctx: StateContext }> {
@@ -48,12 +54,21 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
         detail: 2,
         colorTheme: { name: 'element-symbol' } as ColorTheme,
         colorValue: 0x000000,
+        sizeTheme: { name: 'uniform' } as SizeTheme,
         visible: true,
         alpha: 1,
-        depthMask: true
+        depthMask: true,
+        useFog: true,
+        quality: 'auto' as VisualQuality,
+        unitKinds: [] as Unit.Kind[]
+    }
+
+    componentWillMount() {
+        this.setState({ ...this.state, ...this.props.entity.value.props })
     }
 
     update(state?: Partial<SpacefillState>) {
+        console.log(state)
         const { transform, entity, ctx } = this.props
         const newState = { ...this.state, ...state }
         this.setState(newState)
@@ -63,6 +78,10 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
     render() {
         const { transform } = this.props
 
+        const qualityOptions = ['auto', 'custom', 'highest', 'high', 'medium', 'low', 'lowest'].map((name, idx) => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
         const sphereDetailOptions = [0, 1, 2, 3].map((value, idx) => {
             return <option key={value} value={value}>{value.toString()}</option>
         })
@@ -87,6 +106,18 @@ export class Spacefill extends View<Controller<any>, SpacefillState, { transform
                 </div>
                 <div className='molstar-panel-body'>
                     <div>
+                    <div className='molstar-control-row molstar-options-group'>
+                        <span>Quality</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value={this.state.quality}
+                                    onChange={(e) => this.update({ quality: e.target.value as VisualQuality })}
+                                >
+                                    {qualityOptions}
+                                </select>
+                            </div>
+                        </div>
                         <div className='molstar-control-row molstar-options-group'>
                             <span>Sphere detail</span>
                             <div>

+ 124 - 0
src/mol-app/ui/transform/url-loader.tsx

@@ -0,0 +1,124 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import * as React from 'react'
+import { View } from '../view';
+import { TransformListController } from '../../controller/transform/list';
+import { UrlEntity } from 'mol-view/state/entity';
+import { ModelToStructure, StructureToBallAndStick, StructureToSpacefill, StructureToDistanceRestraint, StructureToBackbone, MmcifUrlToModel, StructureToCartoon } from 'mol-view/state/transform';
+import { StateContext } from 'mol-view/state/context';
+import { SpacefillProps } from 'mol-geo/representation/structure/spacefill';
+import { BallAndStickProps } from 'mol-geo/representation/structure/ball-and-stick';
+import { DistanceRestraintProps } from 'mol-geo/representation/structure/distance-restraint';
+import { BackboneProps } from 'mol-geo/representation/structure/backbone';
+import { CartoonProps } from 'mol-geo/representation/structure/cartoon';
+
+const spacefillProps: SpacefillProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
+const ballAndStickProps: BallAndStickProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    sizeTheme: { name: 'uniform', value: 0.05 },
+    linkRadius: 0.05,
+    quality: 'auto',
+    useFog: false
+}
+
+const distanceRestraintProps: DistanceRestraintProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    linkRadius: 0.5,
+    quality: 'auto',
+    useFog: false
+}
+
+const backboneProps: BackboneProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
+const cartoonProps: CartoonProps = {
+    doubleSided: true,
+    colorTheme: { name: 'chain-id' },
+    quality: 'auto',
+    useFog: false
+}
+
+function getPdbdevUrl(pdbdevId: string) {
+    return `https://pdb-dev.wwpdb.org/static/cif/${pdbdevId}.cif`
+}
+
+const exampleUrls = {
+    PDBDEV_00000001: getPdbdevUrl('PDBDEV_00000001'), // ok
+    PDBDEV_00000002: getPdbdevUrl('PDBDEV_00000002'), // ok
+    PDBDEV_00000003: getPdbdevUrl('PDBDEV_00000003'), // ok
+    PDBDEV_00000004: getPdbdevUrl('PDBDEV_00000004'), // TODO issue with cross-link extraction
+    PDBDEV_00000005: getPdbdevUrl('PDBDEV_00000005'), // ok
+    PDBDEV_00000006: getPdbdevUrl('PDBDEV_00000006'), // TODO only three spacefill atoms rendered
+    PDBDEV_00000007: getPdbdevUrl('PDBDEV_00000007'), // TODO only three spacefill atoms rendered
+    PDBDEV_00000008: getPdbdevUrl('PDBDEV_00000008'), // ok
+    PDBDEV_00000010: getPdbdevUrl('PDBDEV_00000010'), // ok
+    PDBDEV_00000011: getPdbdevUrl('PDBDEV_00000011'), // ok
+    PDBDEV_00000012: getPdbdevUrl('PDBDEV_00000012'), // ok
+    PDBDEV_00000014: getPdbdevUrl('PDBDEV_00000014'), // ok
+    PDBDEV_00000016: getPdbdevUrl('PDBDEV_00000016'),
+}
+
+export class UrlLoader extends View<TransformListController, { }, { ctx: StateContext }> {
+    async load(name: keyof typeof exampleUrls) {
+        console.log(exampleUrls[name])
+        const ctx = this.props.ctx
+        const urlEntity = UrlEntity.ofUrl(ctx, exampleUrls[name])
+        console.log(await urlEntity.value.getData())
+        const modelEntity = await MmcifUrlToModel.apply(ctx, urlEntity)
+        const structureEntity = await ModelToStructure.apply(ctx, modelEntity)
+
+        StructureToBallAndStick.apply(ctx, structureEntity, { ...ballAndStickProps, visible: true })
+        StructureToSpacefill.apply(ctx, structureEntity, { ...spacefillProps, visible: false })
+        StructureToDistanceRestraint.apply(ctx, structureEntity, { ...distanceRestraintProps, visible: false })
+        StructureToBackbone.apply(ctx, structureEntity, { ...backboneProps, visible: true })
+        StructureToCartoon.apply(ctx, structureEntity, { ...cartoonProps, visible: false })
+    }
+
+    render() {
+        const exampleOptions = Object.keys(exampleUrls).map(name => {
+            return <option key={name} value={name}>{name}</option>
+        })
+
+        return <div className='molstar-transformer-wrapper'>
+            <div className='molstar-panel molstar-control molstar-transformer molstar-panel-expanded'>
+                <div className='molstar-panel-body'>
+                    <div>
+                        <div className='molstar-control-row molstar-options-group'>
+                            <span>Examples</span>
+                            <div>
+                                <select
+                                    className='molstar-form-control'
+                                    value=''
+                                    onChange={(e) => {
+                                        if (e.target.value) {
+                                            this.load(e.target.value as keyof typeof exampleUrls)}
+                                        }
+                                    }
+                                >
+                                    <option key='' value=''></option>
+                                    {exampleOptions}
+                                </select>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>;
+    }
+}

+ 6 - 0
src/mol-app/ui/visualization/image-canvas.tsx

@@ -22,6 +22,12 @@ export class ImageCanvas extends React.Component<{ imageData: ImageData, aspectR
     private canvas: HTMLCanvasElement | null = null;
     private ctx: CanvasRenderingContext2D | null = null;
 
+    state = {
+        imageData: new ImageData(1, 1),
+        width: 1,
+        height: 1
+    }
+
     updateStateFromProps() {
         this.setState({
             imageData: this.props.imageData,

+ 85 - 0
src/mol-app/ui/visualization/sequence-view.tsx

@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react'
+import { View } from '../view';
+import { SequenceViewController } from '../../controller/visualization/sequence-view';
+import { Structure, StructureSequence, Queries, StructureSelection, StructureProperties, StructureQuery } from 'mol-model/structure';
+import { Context } from '../../context/context';
+import { InteractivityEvents } from '../../event/basic';
+import { EmptyLoci } from 'mol-model/loci';
+
+export class SequenceView extends View<SequenceViewController, {}, {}> {
+    render() {
+        const s = this.controller.latestState.structure;
+        if (!s) return <div className='molstar-sequence-view-wrap'>No structure available.</div>;
+
+        const seqs = Structure.getModels(s)[0].sequence.sequences;
+        return <div className='molstar-sequence-view-wrap'>
+            {seqs.map((seq, i) => <EntitySequence key={i} ctx={this.controller.context} seq={seq} structure={s} /> )}
+        </div>;
+    }
+}
+
+function createQuery(entityId: string, label_seq_id: number) {
+    return Queries.generators.atoms({
+        entityTest: ctx => StructureProperties.entity.id(ctx.element) === entityId,
+        residueTest: ctx => StructureProperties.residue.label_seq_id(ctx.element) === label_seq_id
+    });
+}
+
+// TODO: this is really ineffective and should be done using a canvas.
+class EntitySequence extends React.Component<{ ctx: Context, seq: StructureSequence.Entity, structure: Structure }> {
+
+    raiseInteractityEvent(seqId?: number) {
+        if (typeof seqId === 'undefined') {
+            InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, EmptyLoci);
+            return;
+        }
+
+        const query = createQuery(this.props.seq.entityId, seqId);
+        const loci = StructureSelection.toLoci(StructureQuery.run(query, this.props.structure));
+        if (loci.elements.length === 0) InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, EmptyLoci);
+        else InteractivityEvents.HighlightLoci.dispatch(this.props.ctx, loci);
+    }
+
+
+    render() {
+        const { ctx, seq } = this.props;
+        const { offset, sequence } = seq.sequence;
+
+        const elems: JSX.Element[] = [];
+        for (let i = 0, _i = sequence.length; i < _i; i++) {
+            elems[elems.length] = <ResidueView ctx={ctx} seqId={offset + i} letter={sequence[i]} parent={this} key={i} />;
+        }
+
+        return <div style={{ wordWrap: 'break-word' }}>
+            <span style={{ fontWeight: 'bold' }}>{this.props.seq.entityId}:{offset}&nbsp;</span>
+            {elems}
+        </div>;
+    }
+}
+
+class ResidueView extends React.Component<{ ctx: Context, seqId: number, letter: string, parent: EntitySequence }, { isHighlighted: boolean }> {
+    state = { isHighlighted: false }
+
+    mouseEnter = () => {
+        this.setState({ isHighlighted: true });
+        this.props.parent.raiseInteractityEvent(this.props.seqId);
+    }
+
+    mouseLeave = () => {
+        this.setState({ isHighlighted: false });
+        this.props.parent.raiseInteractityEvent();
+    }
+
+    render() {
+        return <span onMouseEnter={this.mouseEnter} onMouseLeave={this.mouseLeave}
+            style={{ cursor: 'pointer', backgroundColor: this.state.isHighlighted ? 'yellow' : void 0 }}>
+            {this.props.letter}
+        </span>;
+    }
+}

+ 23 - 5
src/mol-app/ui/visualization/viewport.tsx

@@ -14,6 +14,8 @@ import { View } from '../view';
 import { HelpBox, Toggle, Button } from '../controls/common'
 import { Slider } from '../controls/slider'
 import { ImageCanvas } from './image-canvas';
+import { InteractivityEvents } from '../../event/basic';
+import { labelFirst } from 'mol-view/label';
 
 export class ViewportControls extends View<ViewportController, { showSceneOptions?: boolean, showHelp?: boolean }, {}> {
     state = { showSceneOptions: false, showHelp: false };
@@ -132,6 +134,7 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl?
         if (!this.canvas || !this.container || !this.controller.context.initStage(this.canvas, this.container)) {
             this.setState({ noWebGl: true });
         }
+        this.handleResize()
 
         const viewer = this.controller.context.stage.viewer
 
@@ -142,10 +145,6 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl?
             })
         })
 
-        viewer.identified.subscribe(info => {
-            this.setState({ info })
-        })
-
         viewer.didDraw.subscribe(() => {
             // this.setState({ imageData: viewer.getImageData() })
             this.setState({
@@ -158,7 +157,26 @@ export class Viewport extends View<ViewportController, ViewportState, { noWebGl?
         })
 
         viewer.input.resize.subscribe(() => this.handleResize())
-        this.handleResize()
+
+        // TODO: clear highlight on mouse/touch down?
+
+        viewer.input.move.subscribe(({x, y, inside, buttons}) => {
+            if (!inside || buttons) return
+            const p = viewer.identify(x, y)
+            const loci = viewer.getLoci(p)
+            InteractivityEvents.HighlightLoci.dispatch(this.controller.context, loci);
+
+            // TODO use LabelLoci event and make configurable
+            const label = labelFirst(loci)
+            const info = `Object: ${p.objectId}, Instance: ${p.instanceId}, Element: ${p.elementId}, Label: ${label}`
+            this.setState({ info })
+        })
+
+        // TODO filter only for left button/single finger touch?
+        viewer.input.click.subscribe(({x, y}) => {
+            const loci = viewer.getLoci(viewer.identify(x, y))
+            InteractivityEvents.SelectLoci.dispatch(this.controller.context, loci);
+        })
     }
 
     componentWillUnmount() {

+ 12 - 2
src/mol-data/db/column.ts

@@ -103,6 +103,15 @@ namespace Column {
         return lambdaColumn(spec);
     }
 
+    /** values [min, max] (i.e. include both values) */
+    export function range(min: number, max: number): Column<number> {
+        return ofLambda({
+            value: i => i + min,
+            rowCount: Math.max(max - min + 1, 0),
+            schema: Schema.int
+        });
+    }
+
     export function ofArray<T extends Column.Schema>(spec: Column.ArraySpec<T>): Column<T['T']> {
         return arrayColumn(spec);
     }
@@ -132,8 +141,8 @@ namespace Column {
         return createFirstIndexMapOfColumn(column);
     }
 
-    export function createIndexer<T>(column: Column<T>) {
-        return createIndexerOfColumn(column);
+    export function createIndexer<T, R extends number = number>(column: Column<T>) {
+        return createIndexerOfColumn(column) as ((e: T) => R);
     }
 
     export function mapToArray<T, S>(column: Column<T>, f: (v: T) => S, ctor?: ArrayCtor<S>): ArrayLike<S> {
@@ -261,6 +270,7 @@ function arrayColumn<T extends Column.Schema>({ array, schema, valueKind }: Colu
 
 function windowColumn<T>(column: Column<T>, start: number, end: number) {
     if (!column.isDefined) return Column.Undefined(end - start, column.schema);
+    if (start === 0 && end === column.rowCount) return column;
     if (!!column['@array'] && ColumnHelpers.isTypedArray(column['@array'])) return windowTyped(column, start, end);
     return windowFull(column, start, end);
 }

+ 13 - 0
src/mol-data/db/table.ts

@@ -101,6 +101,19 @@ namespace Table {
         return ret as Table<R>;
     }
 
+    export function window<S extends R, R extends Schema>(table: Table<S>, schema: R, start: number, end: number) {
+        if (start === 0 && end === table._rowCount) return table;
+        const ret = Object.create(null);
+        const columns = Object.keys(schema);
+        ret._rowCount = end - start;
+        ret._columns = columns;
+        ret._schema = schema;
+        for (const k of columns) {
+            (ret as any)[k] = Column.window(table[k], start, end);
+        }
+        return ret as Table<R>;
+    }
+
     export function concat<S extends R, R extends Schema>(tables: Table<S>[], schema: R) {
         const ret = Object.create(null);
         const columns = Object.keys(schema);

+ 6 - 0
src/mol-data/int/_spec/interval.spec.ts

@@ -73,4 +73,10 @@ describe('interval', () => {
     test('predIndexInt3', Interval.findPredecessorIndexInInterval(Interval.ofRange(3, 10), 5, Interval.ofRange(2, 6)), 2);
 
     testI('findRange', Interval.findRange(r05, 2, 3), Interval.ofRange(2, 3));
+
+    test('intersectionSize1', Interval.intersectionSize(Interval.ofRange(0, 5), Interval.ofRange(0, 5)), 6);
+    test('intersectionSize2', Interval.intersectionSize(Interval.ofRange(0, 5), Interval.ofRange(1, 2)), 2);
+    test('intersectionSize3', Interval.intersectionSize(Interval.ofRange(1, 2), Interval.ofRange(0, 5)), 2);
+    test('intersectionSize4', Interval.intersectionSize(Interval.ofRange(0, 5), Interval.ofRange(3, 8)), 3);
+    test('intersectionSize5', Interval.intersectionSize(Interval.ofRange(0, 5), Interval.ofRange(6, 8)), 0);
 });

+ 25 - 0
src/mol-data/int/_spec/ordered-set.spec.ts

@@ -25,6 +25,7 @@ describe('ordered set', () => {
     const singleton10 = OrderedSet.ofSingleton(10);
     const range1_4 = OrderedSet.ofRange(1, 4);
     const arr136 = OrderedSet.ofSortedArray([1, 3, 6]);
+    const arr12369 = OrderedSet.ofSortedArray([1, 2, 3, 6, 9]);
 
     const iB = (s: number, e: number) => Interval.ofBounds(s, e);
 
@@ -47,6 +48,17 @@ describe('ordered set', () => {
         expect(OrderedSet.areIntersecting(empty, singleton10)).toBe(false);
         expect(OrderedSet.areIntersecting(empty, range1_4)).toBe(false);
         expect(OrderedSet.areIntersecting(empty, arr136)).toBe(false);
+
+        expect(OrderedSet.areIntersecting(Interval.ofRange(2, 3), arr12369)).toBe(true);
+        expect(OrderedSet.areIntersecting(Interval.ofRange(2, 6), arr12369)).toBe(true);
+        expect(OrderedSet.areIntersecting(Interval.ofRange(2, 8), arr12369)).toBe(true);
+        expect(OrderedSet.areIntersecting(Interval.ofRange(4, 8), arr12369)).toBe(true);
+
+        expect(OrderedSet.areIntersecting(Interval.ofRange(4, 5), arr12369)).toBe(false);
+        expect(OrderedSet.areIntersecting(Interval.ofRange(7, 8), arr12369)).toBe(false);
+
+        expect(OrderedSet.areIntersecting(Interval.ofRange(6, 6), arr12369)).toBe(true);
+        expect(OrderedSet.areIntersecting(Interval.ofRange(3, 4), OrderedSet.ofSortedArray([0, 1, 10]))).toBe(false);
     });
 
     it('isSubset', () => {
@@ -104,6 +116,13 @@ describe('ordered set', () => {
         expect(OrderedSet.findRange(arr136, 2, 7)).toEqual(iB(1, 3));
     })
 
+    it('intersectionSize', () => {
+        expect(OrderedSet.intersectionSize(arr136, range1_4)).toEqual(2);
+        expect(OrderedSet.intersectionSize(arr12369, range1_4)).toEqual(3);
+        expect(OrderedSet.intersectionSize(OrderedSet.ofSortedArray([12, 13, 16]), range1_4)).toEqual(0);
+        expect(OrderedSet.intersectionSize(OrderedSet.ofSortedArray([1, 2, 4]), range1_4)).toEqual(3);
+    })
+
     testEq('union ES', OrderedSet.union(empty, singleton10), [10]);
     testEq('union ER', OrderedSet.union(empty, range1_4), [1, 2, 3, 4]);
     testEq('union EA', OrderedSet.union(empty, arr136), [1, 3, 6]);
@@ -157,4 +176,10 @@ describe('ordered set', () => {
     testEq('subtract AA', OrderedSet.subtract(arr136, arr136), []);
     testEq('subtract AA1', OrderedSet.subtract(arr136, OrderedSet.ofSortedArray([2, 3, 4, 6, 7])), [1]);
     testEq('subtract AA2', OrderedSet.subtract(arr136, OrderedSet.ofSortedArray([0, 1, 6])), [3]);
+
+    it('foreach', () => {
+        const int = OrderedSet.ofBounds(1, 3), set = OrderedSet.ofSortedArray([2, 3, 4]);
+        expect(OrderedSet.forEach(int, (v, i, ctx) => ctx[i] = v, [] as number[])).toEqual([1, 2]);
+        expect(OrderedSet.forEach(set, (v, i, ctx) => ctx[i] = v, [] as number[])).toEqual([2, 3, 4]);
+    })
 });

+ 2 - 2
src/mol-data/int/_spec/segmentation.spec.ts

@@ -21,12 +21,12 @@ describe('segments', () => {
 
     it('ofOffsetts', () => {
         const p = Segmentation.ofOffsets([10, 12], Interval.ofBounds(10, 14));
-        expect(p.segments).toEqual(new Int32Array([0, 2, 4]))
+        expect(p.offsets).toEqual(new Int32Array([0, 2, 4]))
     });
 
     it('map', () => {
         const segs = Segmentation.create([0, 1, 2]);
-        expect(segs.segmentMap).toEqual(new Int32Array([0, 1]));
+        expect(segs.index).toEqual(new Int32Array([0, 1]));
         expect(Segmentation.getSegment(segs, 0)).toBe(0);
         expect(Segmentation.getSegment(segs, 1)).toBe(1);
     });

+ 6 - 0
src/mol-data/int/_spec/sorted-array.spec.ts

@@ -59,6 +59,12 @@ describe('sortedArray', () => {
         compareArrays(SortedArray.deduplicate(SortedArray.ofSortedArray([1, 2, 3])), [1, 2, 3]);
     });
 
+    it('indicesOf', () => {
+        compareArrays(SortedArray.indicesOf(SortedArray.ofSortedArray([10, 11, 12]), SortedArray.ofSortedArray([10, 12, 14])), [0, 2]);
+    })
+
+    test('intersectionSize', SortedArray.intersectionSize(a1234, a2468), 2);
+
     // console.log(Interval.findPredecessorIndexInInterval(Interval.ofBounds(0, 3), 2, Interval.ofBounds(0, 3)))
     // console.log(SortedArray.findPredecessorIndexInInterval(SortedArray.ofSortedArray([0, 1, 2]), 2, Interval.ofBounds(0, 3)))
 });

+ 72 - 0
src/mol-data/int/_spec/sorted-ranges.spec.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import SortedRanges from '../sorted-ranges'
+import OrderedSet from '../ordered-set';
+import SortedArray from '../sorted-array';
+
+describe('rangesArray', () => {
+    function test(name: string, a: any, b: any) {
+        it(name, () => expect(a).toEqual(b));
+    }
+
+    function testIterator(name: string, ranges: SortedRanges, set: OrderedSet, expectedValues: { index: number[], start: number[], end: number[]}) {
+        it(`iterator, ${name}`, () => {
+            const rangesIt = SortedRanges.transientSegments(ranges, set)
+            const { index, start, end } = expectedValues
+    
+            let i = 0
+            while (rangesIt.hasNext) {
+                const segment = rangesIt.move()
+                expect(segment.index).toBe(index[i])
+                expect(segment.start).toBe(start[i])
+                expect(segment.end).toBe(end[i])
+                ++i
+            }
+            expect(i).toBe(index.length)
+        })
+    }
+
+    const a1234 = SortedRanges.ofSortedRanges([1, 2, 3, 4]);
+    const a1134 = SortedRanges.ofSortedRanges([1, 1, 3, 4]);
+
+    test('size', SortedRanges.size(a1234), 4);
+    test('size', SortedRanges.size(a1134), 3);
+
+    test('min/max', [SortedRanges.min(a1234), SortedRanges.max(a1234)], [1, 4]);
+    test('start/end', [SortedRanges.start(a1234), SortedRanges.end(a1234)], [1, 5]);
+
+    testIterator('two ranges',
+        SortedRanges.ofSortedRanges([1, 2, 3, 4]),
+        OrderedSet.ofBounds(1, 4),
+        { index: [0, 1], start: [0, 2], end: [2, 4] }
+    )
+    testIterator('first range',
+        SortedRanges.ofSortedRanges([1, 2, 3, 4]),
+        SortedArray.ofSortedArray([2]),
+        { index: [0], start: [0], end: [1] }
+    )
+    testIterator('second range',
+        SortedRanges.ofSortedRanges([1, 2, 3, 4]),
+        SortedArray.ofSortedArray([4]),
+        { index: [1], start: [0], end: [1] }
+    )
+    testIterator('set not in ranges',
+        SortedRanges.ofSortedRanges([1, 2, 3, 4]),
+        SortedArray.ofSortedArray([10]),
+        { index: [], start: [], end: [] }
+    )
+    testIterator('set in second range and beyond',
+        SortedRanges.ofSortedRanges([1, 2, 3, 4]),
+        SortedArray.ofSortedArray([3, 10]),
+        { index: [1], start: [0], end: [2] }
+    )
+    testIterator('length 1 range',
+        SortedRanges.ofSortedRanges([1, 1, 3, 4]),
+        SortedArray.ofSortedArray([0, 1, 10]),
+        { index: [0], start: [1], end: [2] }
+    )
+});

+ 4 - 0
src/mol-data/int/impl/interval.ts

@@ -58,4 +58,8 @@ export function findRange(int: Tuple, min: number, max: number) {
 export function intersect(a: Tuple, b: Tuple) {
     if (!areIntersecting(a, b)) return Empty;
     return ofBounds(Math.max(start(a), start(b)), Math.min(end(a), end(b)));
+}
+
+export function intersectionSize(a: Tuple, b: Tuple) {
+    return size(findRange(a, min(b), max(b)))
 }

+ 31 - 1
src/mol-data/int/impl/ordered-set.ts

@@ -29,6 +29,8 @@ export function indexOf(set: OrderedSetImpl, x: number) { return I.is(set) ? I.i
 export function getAt(set: OrderedSetImpl, i: number) { return I.is(set) ? I.getAt(set, i) : set[i]; }
 export function min(set: OrderedSetImpl) { return I.is(set) ? I.min(set) : S.min(set); }
 export function max(set: OrderedSetImpl) { return I.is(set) ? I.max(set) : S.max(set); }
+export function start(set: OrderedSetImpl) { return I.is(set) ? I.start(set) : S.start(set); }
+export function end(set: OrderedSetImpl) { return I.is(set) ? I.end(set) : S.end(set); }
 
 export function hashCode(set: OrderedSetImpl) { return I.is(set) ? I.hashCode(set) : S.hashCode(set); }
 // TODO: possibly add more hash functions to allow for multilevel hashing.
@@ -70,6 +72,14 @@ export function findRange(set: OrderedSetImpl, min: number, max: number) {
     return I.is(set) ? I.findRange(set, min, max) : S.findRange(set, min, max);
 }
 
+export function intersectionSize(a: OrderedSetImpl, b: OrderedSetImpl): number {
+    if (I.is(a)) {
+        if (I.is(b)) return I.intersectionSize(a, b);
+        return intersectionSizeSI(b, a);
+    } else if (I.is(b)) return intersectionSizeSI(a, b);
+    return S.intersectionSize(a, b);
+}
+
 export function union(a: OrderedSetImpl, b: OrderedSetImpl) {
     if (I.is(a)) {
         if (I.is(b)) return unionII(a, b);
@@ -97,7 +107,7 @@ export function subtract(a: OrderedSetImpl, b: OrderedSetImpl) {
 function areEqualIS(a: I, b: S) { return I.size(a) === S.size(b) && I.start(a) === S.start(b) && I.end(a) === S.end(b); }
 
 function areIntersectingSI(a: S, b: I) {
-    return areRangesIntersecting(a, b);
+    return a.length !== 0 && I.size(S.findRange(a, I.min(b), I.max(b))) !== 0;
 }
 
 function isSubsetSI(a: S, b: I) {
@@ -163,6 +173,12 @@ function unionSI(a: S, b: I) {
     return ofSortedArray(indices);
 }
 
+function intersectionSizeSI(a: S, b: I): number {
+    if (!I.size(b)) return 0;
+    const r = S.findRange(a, I.min(b), I.max(b));
+    return I.end(r) - I.start(r);
+}
+
 function intersectSI(a: S, b: I) {
     if (!I.size(b)) return Empty;
 
@@ -253,4 +269,18 @@ function subtractIS(a: I, b: S) {
     }
     for (let i = last + 1; i <= max; i++) ret[offset++] = i;
     return ofSortedArray(ret);
+}
+
+export function forEach(set: OrderedSetImpl, f: (value: number, i: number, ctx: any) => void, ctx: any) {
+    if (I.is(set)) {
+        const start = I.min(set);
+        for (let i = start, _i = I.max(set); i <= _i; i++) {
+            f(i, i - start, ctx);
+        }
+    } else {
+        for (let i = 0, _i = set.length; i < _i; i++) {
+            f(set[i], i, ctx);
+        }
+    }
+    return ctx;
 }

+ 18 - 19
src/mol-data/int/impl/segmentation.ts

@@ -12,23 +12,23 @@ import Segs from '../segmentation'
 
 interface Segmentation {
     /** Segments stored as a sorted array */
-    segments: SortedArray,
+    offsets: SortedArray,
     /** Mapping of values to segments */
-    segmentMap: Int32Array,
+    index: Int32Array,
     /** Number of segments */
     count: number
 }
 
 export function create(values: ArrayLike<number>): Segmentation {
-    const segments = SortedArray.ofSortedArray(values);
-    const max = SortedArray.max(segments);
-    const segmentMap = new Int32Array(max);
+    const offsets = SortedArray.ofSortedArray(values);
+    const max = SortedArray.max(offsets);
+    const index = new Int32Array(max);
     for (let i = 0, _i = values.length - 1; i < _i; i++) {
         for (let j = values[i], _j = values[i + 1]; j < _j; j++) {
-            segmentMap[j] = i;
+            index[j] = i;
         }
     }
-    return { segments, segmentMap, count: values.length - 1 };
+    return { offsets, index, count: values.length - 1 };
 }
 
 export function ofOffsets(offsets: ArrayLike<number>, bounds: Interval): Segmentation {
@@ -41,27 +41,28 @@ export function ofOffsets(offsets: ArrayLike<number>, bounds: Interval): Segment
     return create(segments);
 }
 
+/** Get number of segments in a segmentation */
 export function count({ count }: Segmentation) { return count; }
-export function getSegment({ segmentMap }: Segmentation, value: number) { return segmentMap[value]; }
+export function getSegment({ index }: Segmentation, value: number) { return index[value]; }
 
-export function projectValue({ segments }: Segmentation, set: OrderedSet, value: number): Interval {
-    const last = OrderedSet.max(segments);
-    const idx = value >= last ? -1 : OrderedSet.findPredecessorIndex(segments, value - 1);
-    return OrderedSet.findRange(set, OrderedSet.getAt(segments, idx), OrderedSet.getAt(segments, idx + 1) - 1);
+export function projectValue({ offsets }: Segmentation, set: OrderedSet, value: number): Interval {
+    const last = OrderedSet.max(offsets);
+    const idx = value >= last ? -1 : OrderedSet.findPredecessorIndex(offsets, value - 1);
+    return OrderedSet.findRange(set, OrderedSet.getAt(offsets, idx), OrderedSet.getAt(offsets, idx + 1) - 1);
 }
 
-export class SegmentIterator implements Iterator<Segs.Segment> {
+export class SegmentIterator<I extends number = number> implements Iterator<Segs.Segment<I>> {
     private segmentMin = 0;
     private segmentMax = 0;
     private setRange = Interval.Empty;
-    private value: Segs.Segment = { index: 0, start: 0, end: 0 };
+    private value: Segs.Segment<I> = { index: 0 as I, start: 0, end: 0 };
 
     hasNext: boolean = false;
 
     move() {
         while (this.hasNext) {
             if (this.updateValue()) {
-                this.value.index = this.segmentMin++;
+                this.value.index = this.segmentMin++ as I;
                 this.hasNext = this.segmentMax >= this.segmentMin && Interval.size(this.setRange) > 0;
                 break;
             } else {
@@ -87,14 +88,12 @@ export class SegmentIterator implements Iterator<Segs.Segment> {
             this.hasNext = false;
             return;
         }
-
         this.segmentMin = this.segmentMap[OrderedSet.getAt(this.set, sMin)];
         this.segmentMax = this.segmentMap[OrderedSet.getAt(this.set, sMax)];
-
         this.hasNext = this.segmentMax >= this.segmentMin;
     }
 
-    setSegment(segment: Segs.Segment) {
+    setSegment(segment: Segs.Segment<number>) {
         this.setRange = Interval.ofBounds(segment.start, segment.end);
         this.updateSegmentRange();
     }
@@ -107,5 +106,5 @@ export class SegmentIterator implements Iterator<Segs.Segment> {
 
 export function segments(segs: Segmentation, set: OrderedSet, segment?: Segs.Segment) {
     const int = typeof segment !== 'undefined' ? Interval.ofBounds(segment.start, segment.end) : Interval.ofBounds(0, OrderedSet.size(set));
-    return new SegmentIterator(segs.segments, segs.segmentMap, set, int);
+    return new SegmentIterator(segs.offsets, segs.index, set, int);
 }

+ 60 - 20
src/mol-data/int/impl/sorted-array.ts

@@ -4,7 +4,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { sortArray, hash3, hash4 } from '../../util'
+import { sortArray, hash3, hash4, createRangeArray } from '../../util'
 import Interval from '../interval'
 
 type Nums = ArrayLike<number>
@@ -71,6 +71,7 @@ export function findPredecessorIndexInInterval(xs: Nums, v: number, bounds: Inte
     const sv = xs[s];
     if (v <= sv) return s;
     if (e > s && v > xs[e - 1]) return e;
+    // do a linear search if there are only 10 or less items remaining
     if (v - sv <= 11) return linearSearchPredInRange(xs, v, s + 1, e);
     return binarySearchPredIndexRange(xs, v, s, e);
 }
@@ -82,6 +83,7 @@ export function findRange(xs: Nums, min: number, max: number) {
 function binarySearchRange(xs: Nums, value: number, start: number, end: number) {
     let min = start, max = end - 1;
     while (min <= max) {
+        // do a linear search if there are only 10 or less items remaining
         if (min + 11 > max) {
             for (let i = min; i <= max; i++) {
                 if (value === xs[i]) return i;
@@ -101,6 +103,7 @@ function binarySearchRange(xs: Nums, value: number, start: number, end: number)
 function binarySearchPredIndexRange(xs: Nums, value: number, start: number, end: number) {
     let min = start, max = end - 1;
     while (min < max) {
+        // do a linear search if there are only 10 or less items remaining
         if (min + 11 > max) {
             for (let i = min; i <= max; i++) {
                 if (value <= xs[i]) return i;
@@ -158,15 +161,8 @@ export function isSubset(a: Nums, b: Nums) {
 export function union(a: Nums, b: Nums) {
     if (a === b) return a;
 
-    const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
-    let i = sI, j = sJ;
-    let commonCount = 0;
-    while (i < endI && j < endJ) {
-        const x = a[i], y = b[j];
-        if (x < y) { i++; }
-        else if (x > y) { j++; }
-        else { i++; j++; commonCount++; }
-    }
+    const { startI, startJ, endI, endJ } = getSuitableIntersectionRange(a, b);
+    const commonCount = getCommonCount(a, b, startI, startJ, endI, endJ);
 
     const lenA = a.length, lenB = b.length;
     // A === B || B is subset of A ==> A
@@ -178,12 +174,12 @@ export function union(a: Nums, b: Nums) {
     let offset = 0;
 
     // insert the "prefixes"
-    for (let k = 0; k < sI; k++) indices[offset++] = a[k];
-    for (let k = 0; k < sJ; k++) indices[offset++] = b[k];
+    for (let k = 0; k < startI; k++) indices[offset++] = a[k];
+    for (let k = 0; k < startJ; k++) indices[offset++] = b[k];
 
     // insert the common part
-    i = sI;
-    j = sJ;
+    let i = startI;
+    let j = startJ;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
         if (x < y) { indices[offset++] = x; i++; }
@@ -198,11 +194,14 @@ export function union(a: Nums, b: Nums) {
     return ofSortedArray(indices);
 }
 
-export function intersect(a: Nums, b: Nums) {
-    if (a === b) return a;
+export function intersectionSize(a: Nums, b: Nums) {
+    if (a === b) return size(a);
+    const { startI, startJ, endI, endJ } = getSuitableIntersectionRange(a, b);
+    return getCommonCount(a, b, startI, startJ, endI, endJ);
+}
 
-    const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
-    let i = sI, j = sJ;
+function getCommonCount(a: Nums, b: Nums, startI: number, startJ: number, endI: number, endJ: number) {
+    let i = startI, j = startJ;
     let commonCount = 0;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
@@ -210,6 +209,14 @@ export function intersect(a: Nums, b: Nums) {
         else if (x > y) { j++; }
         else { i++; j++; commonCount++; }
     }
+    return commonCount;
+}
+
+export function intersect(a: Nums, b: Nums) {
+    if (a === b) return a;
+
+    const { startI, startJ, endI, endJ } = getSuitableIntersectionRange(a, b);
+    const commonCount = getCommonCount(a, b, startI, startJ, endI, endJ);
 
     const lenA = a.length, lenB = b.length;
     // no common elements
@@ -221,8 +228,8 @@ export function intersect(a: Nums, b: Nums) {
 
     const indices = new Int32Array(commonCount);
     let offset = 0;
-    i = sI;
-    j = sJ;
+    let i = startI;
+    let j = startJ;
     while (i < endI && j < endJ) {
         const x = a[i], y = b[j];
         if (x < y) { i++; }
@@ -289,6 +296,39 @@ export function deduplicate(xs: Nums) {
     return ret;
 }
 
+export function indicesOf(a: Nums, b: Nums): Nums {
+    if (a === b) return ofSortedArray(createRangeArray(0, a.length - 1));
+
+    const { startI: sI, startJ: sJ, endI, endJ } = getSuitableIntersectionRange(a, b);
+    let i = sI, j = sJ;
+    let commonCount = 0;
+    while (i < endI && j < endJ) {
+        const x = a[i], y = b[j];
+        if (x < y) { i++; }
+        else if (x > y) { j++; }
+        else { i++; j++; commonCount++; }
+    }
+
+    const lenA = a.length;
+    // no common elements
+    if (!commonCount) return Empty;
+    // A is subset of B ==> A
+    if (commonCount === lenA) return ofSortedArray(createRangeArray(0, a.length - 1));
+
+    const indices = new Int32Array(commonCount);
+    let offset = 0;
+    i = sI;
+    j = sJ;
+    while (i < endI && j < endJ) {
+        const x = a[i], y = b[j];
+        if (x < y) { i++; }
+        else if (x > y) { j++; }
+        else { indices[offset++] = i; i++; j++; }
+    }
+
+    return ofSortedArray(indices);
+}
+
 const _maxIntRangeRet = { startI: 0, startJ: 0, endI: 0, endJ: 0 };
 // for small sets, just gets the whole range, for large sets does a bunch of binary searches
 function getSuitableIntersectionRange(a: Nums, b: Nums) {

+ 28 - 26
src/mol-data/int/interval.ts

@@ -9,48 +9,50 @@ import * as Impl from './impl/interval'
 namespace Interval {
     export const Empty: Interval = Impl.Empty as any;
 
-    export const ofSingleton: (value: number) => Interval = (v) => Impl.ofRange(v, v) as any;
+    export const ofSingleton: <T extends number = number>(value: T) => Interval<T> = (v) => Impl.ofRange(v, v) as any;
     /** Create interval from range [min, max] */
-    export const ofRange: (min: number, max: number) => Interval = Impl.ofRange as any;
+    export const ofRange: <T extends number = number>(min: T, max: T) => Interval<T> = Impl.ofRange as any;
     /** Create interval from bounds [start, end), i.e. [start, end - 1] */
-    export const ofBounds: (start: number, end: number) => Interval = Impl.ofBounds as any;
-    export const is: (v: any) => v is Interval = Impl.is as any;
+    export const ofBounds: <T extends number = number>(start: T, end: T) => Interval<T> = Impl.ofBounds as any;
+    export const is: <T extends number = number>(v: any) => v is Interval<T> = Impl.is as any;
 
     /** Test if a value is within the bounds of the interval */
-    export const has: (interval: Interval, x: number) => boolean = Impl.has as any;
-    export const indexOf: (interval: Interval, x: number) => number = Impl.indexOf as any;
-    export const getAt: (interval: Interval, i: number) => number = Impl.getAt as any;
-
-    /** Start value of the interval, same as min value */
-    export const start: (interval: Interval) => number = Impl.start as any;
-    /** End value of the interval, same as max + 1 */
-    export const end: (interval: Interval) => number = Impl.end as any;
-    /** Min value of the interval, same as start value */
-    export const min: (interval: Interval) => number = Impl.min as any;
-    /** Max value of the interval, same as end - 1 */
-    export const max: (interval: Interval) => number = Impl.max as any;
+    export const has: <T extends number = number>(interval: Interval<T>, x: T) => boolean = Impl.has as any;
+    export const indexOf: <T extends number = number>(interval: Interval<T>, x: T) => number = Impl.indexOf as any;
+    export const getAt: <T extends number = number>(interval: Interval<T>, i: number) => T = Impl.getAt as any;
+
+    /** Start value of the Interval<T>, same as min value */
+    export const start: <T extends number = number>(interval: Interval<T>) => T = Impl.start as any;
+    /** End value of the Interval<T>, same as max + 1 */
+    export const end: <T extends number = number>(interval: Interval<T>) => T = Impl.end as any;
+    /** Min value of the Interval<T>, same as start value */
+    export const min: <T extends number = number>(interval: Interval<T>) => T = Impl.min as any;
+    /** Max value of the Interval<T>, same as end - 1 */
+    export const max: <T extends number = number>(interval: Interval<T>) => T = Impl.max as any;
     /** Number of values in the interval */
-    export const size: (interval: Interval) => number = Impl.size as any;
+    export const size: <T extends number = number>(interval: Interval<T>) => number = Impl.size as any;
     /** Hash code describing the interval */
-    export const hashCode: (interval: Interval) => number = Impl.hashCode as any;
+    export const hashCode: <T extends number = number>(interval: Interval<T>) => number = Impl.hashCode as any;
 
     /** Test if two intervals are identical */
-    export const areEqual: (a: Interval, b: Interval) => boolean = Impl.areEqual as any;
+    export const areEqual: <T extends number = number>(a: Interval<T>, b: Interval<T>) => boolean = Impl.areEqual as any;
     /** Test if two intervals are intersecting, i.e. their bounds overlap */
-    export const areIntersecting: (a: Interval, b: Interval) => boolean = Impl.areIntersecting as any;
+    export const areIntersecting: <T extends number = number>(a: Interval<T>, b: Interval<T>) => boolean = Impl.areIntersecting as any;
 
     /** Test if interval b is fully included in interval a */
-    export const isSubInterval: (a: Interval, b: Interval) => boolean = Impl.isSubInterval as any;
+    export const isSubInterval: <T extends number = number>(a: Interval<T>, b: Interval<T>) => boolean = Impl.isSubInterval as any;
 
-    export const findPredecessorIndex: (interval: Interval, x: number) => number = Impl.findPredecessorIndex as any;
-    export const findPredecessorIndexInInterval: (interval: Interval, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
-    export const findRange: (interval: Interval, min: number, max: number) => Interval = Impl.findRange as any;
+    export const findPredecessorIndex: <T extends number = number>(interval: Interval<T>, x: T) => number = Impl.findPredecessorIndex as any;
+    export const findPredecessorIndexInInterval: <T extends number = number>(interval: Interval<T>, x: T, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
+    export const findRange: <T extends number = number>(interval: Interval<T>, min: T, max: T) => Interval = Impl.findRange as any;
+    /** Size of the intersection of the two intervals */
+    export const intersectionSize: <T extends number = number>(a: Interval<T>, b: Interval<T>) => number = Impl.intersectionSize as any;
 
     /** Get a new interval that is the intersection of the two intervals */
-    export const intersect: (a: Interval, b: Interval) => Interval = Impl.intersect as any;
+    export const intersect: <T extends number = number>(a: Interval<T>, b: Interval<T>) => Interval<T> = Impl.intersect as any;
 }
 
 /** Interval describing a range [min, max] of values */
-interface Interval { '@type': 'int-interval' }
+interface Interval<T extends number = number> { '@type': 'int-interval' }
 
 export default Interval

+ 47 - 21
src/mol-data/int/ordered-set.ts

@@ -6,37 +6,63 @@
 
 import * as Base from './impl/ordered-set'
 import Interval from './interval'
+import SortedArray from './sorted-array';
 
+/** test */
 namespace OrderedSet {
     export const Empty: OrderedSet = Base.Empty as any;
-    export const ofSingleton: (value: number) => OrderedSet = Base.ofSingleton as any;
-    export const ofRange: (min: number, max: number) => OrderedSet = Base.ofRange as any;
-    export const ofBounds: (min: number, max: number) => OrderedSet = Base.ofBounds as any;
+    export const ofSingleton: <T extends number = number>(value: T) => OrderedSet<T> = Base.ofSingleton as any;
+    export const ofRange: <T extends number = number>(min: T, max: T) => OrderedSet<T> = Base.ofRange as any;
+    export const ofBounds: <T extends number = number>(min: T, max: T) => OrderedSet<T> = Base.ofBounds as any;
     /** It is the responsibility of the caller to ensure the array is sorted and contains unique values. */
-    export const ofSortedArray: (xs: ArrayLike<number>) => OrderedSet = Base.ofSortedArray as any;
+    export const ofSortedArray: <T extends number = number>(xs: ArrayLike<T>) => OrderedSet<T> = Base.ofSortedArray as any;
 
-    export const has: (set: OrderedSet, x: number) => boolean = Base.has as any;
-    export const indexOf: (set: OrderedSet, x: number) => number = Base.indexOf as any;
-    export const getAt: (set: OrderedSet, i: number) => number = Base.getAt as any;
+    export const has: <T extends number = number>(set: OrderedSet<T>, x: T) => boolean = Base.has as any;
+    export const indexOf: <T extends number = number>(set: OrderedSet<T>, x: T) => number = Base.indexOf as any;
+    export const getAt: <T extends number = number>(set: OrderedSet<T>, i: number) => T = Base.getAt as any;
 
-    export const min: (set: OrderedSet) => number = Base.min as any;
-    export const max: (set: OrderedSet) => number = Base.max as any;
-    export const size: (set: OrderedSet) => number = Base.size as any;
-    export const hashCode: (set: OrderedSet) => number = Base.hashCode as any;
+    export const min: <T extends number = number>(set: OrderedSet<T>) => T = Base.min as any;
+    export const max: <T extends number = number>(set: OrderedSet<T>) => T = Base.max as any;
+    export const start: <T extends number = number>(set: OrderedSet<T>) => T = Base.start as any;
+    export const end: <T extends number = number>(set: OrderedSet<T>) => T = Base.end as any;
+    export const size: <T extends number = number>(set: OrderedSet<T>) => number = Base.size as any;
+    export const hashCode: <T extends number = number>(set: OrderedSet<T>) => number = Base.hashCode as any;
 
-    export const areEqual: (a: OrderedSet, b: OrderedSet) => boolean = Base.areEqual as any;
-    export const areIntersecting: (a: OrderedSet, b: OrderedSet) => boolean = Base.areIntersecting as any;
-    export const isSubset: (a: OrderedSet, b: OrderedSet) => boolean = Base.isSubset as any;
+    export const areEqual: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => boolean = Base.areEqual as any;
+    export const areIntersecting: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => boolean = Base.areIntersecting as any;
+    /** Check if the 2nd argument is a subset of the 1st */
+    export const isSubset: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => boolean = Base.isSubset as any;
 
-    export const union: (a: OrderedSet, b: OrderedSet) => OrderedSet = Base.union as any;
-    export const intersect: (a: OrderedSet, b: OrderedSet) => OrderedSet = Base.intersect as any;
-    export const subtract: (a: OrderedSet, b: OrderedSet) => OrderedSet = Base.subtract as any;
+    export const union: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => OrderedSet<T> = Base.union as any;
+    export const intersect: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => OrderedSet<T> = Base.intersect as any;
+    export const subtract: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => OrderedSet<T> = Base.subtract as any;
 
-    export const findPredecessorIndex: (set: OrderedSet, x: number) => number = Base.findPredecessorIndex as any;
-    export const findPredecessorIndexInInterval: (set: OrderedSet, x: number, range: Interval) => number = Base.findPredecessorIndexInInterval as any;
-    export const findRange: (set: OrderedSet, min: number, max: number) => Interval = Base.findRange as any;
+    export const findPredecessorIndex: <T extends number = number>(set: OrderedSet<T>, x: number) => number = Base.findPredecessorIndex as any;
+    export const findPredecessorIndexInInterval: <T extends number = number>(set: OrderedSet<T>, x: T, range: Interval) => number = Base.findPredecessorIndexInInterval as any;
+    export const findRange: <T extends number = number>(set: OrderedSet<T>, min: T, max: T) => Interval = Base.findRange as any;
+    export const intersectionSize: <T extends number = number>(a: OrderedSet<T>, b: OrderedSet<T>) => number = Base.intersectionSize as any;
+
+    export function forEach<T extends number, Ctx>(set: OrderedSet<T>, f: (v: T, i: number, ctx: Ctx) => void, ctx?: Ctx): Ctx {
+        return Base.forEach(set as any, f as any, ctx);
+    }
+
+    export function isInterval<T extends number = number>(set: OrderedSet<T>): set is Interval<T> {
+        return Interval.is(set);
+    }
+
+    export function isSortedArray<T extends number = number>(set: OrderedSet<T>): set is SortedArray<T> {
+        return !Interval.is(set);
+    }
+
+    export function toArray<T extends number = number>(set: OrderedSet<T>): T[] {
+        const array: T[] = []
+        OrderedSet.forEach(set, v => array.push(v))
+        return array
+    }
 }
 
-interface OrderedSet { '@type': 'int-interval' | 'int-sorted-array' }
+/** Represents bla */
+type OrderedSet<T extends number = number> = SortedArray<T> | Interval<T>
+
 
 export default OrderedSet

+ 14 - 10
src/mol-data/int/segmentation.ts

@@ -9,23 +9,27 @@ import OrderedSet from './ordered-set'
 import * as Impl from './impl/segmentation'
 
 namespace Segmentation {
-    export interface Segment { index: number, start: number, end: number }
+    export interface Segment<I extends number = number> { index: I, start: number, end: number }
 
-    export const create: (segs: ArrayLike<number>) => Segmentation = Impl.create as any;
-    export const ofOffsets: (offsets: ArrayLike<number>, bounds: Interval) => Segmentation = Impl.ofOffsets as any;
+    export const create: <T extends number = number, I extends number = number>(segs: ArrayLike<T>) => Segmentation<T, I> = Impl.create as any;
+    export const ofOffsets: <T extends number = number, I extends number = number>(offsets: ArrayLike<T>, bounds: Interval) => Segmentation<T, I> = Impl.ofOffsets as any;
 
-    export const count: (segs: Segmentation) => number = Impl.count as any;
-    export const getSegment: (segs: Segmentation, value: number) => number = Impl.getSegment as any;
-    export const projectValue: (segs: Segmentation, set: OrderedSet, value: number) => Interval = Impl.projectValue as any;
+    export const count: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>) => number = Impl.count as any;
+    export const getSegment: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, value: T) => number = Impl.getSegment as any;
+    export const projectValue: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, set: OrderedSet<T>, value: T) => Interval = Impl.projectValue as any;
 
     // Segment iterator that mutates a single segment object to mark all the segments.
-    export const transientSegments: (segs: Segmentation, set: OrderedSet, segment?: Segment) => Impl.SegmentIterator = Impl.segments as any;
+    export const transientSegments: <T extends number = number, I extends number = number>(segs: Segmentation<T, I>, set: OrderedSet<T>, segment?: Segment) => Impl.SegmentIterator<I> = Impl.segments as any;
+
+    export type SegmentIterator<I extends number = number> = Impl.SegmentIterator<I>
 }
 
-interface Segmentation {
+interface Segmentation<T extends number = number, I extends number = number> {
     '@type': 'segmentation',
-    readonly segments: ArrayLike<number>,
-    readonly segmentMap: ArrayLike<number>,
+    /** All segments are defined by offsets [offsets[i], offsets[i + 1]) for i \in [0, count - 1] */
+    readonly offsets: ArrayLike<T>,
+    /** Segment index of the i-th element */
+    readonly index: ArrayLike<I>,
     readonly count: number
 }
 

+ 38 - 33
src/mol-data/int/sorted-array.ts

@@ -9,41 +9,46 @@ import Interval from './interval'
 
 namespace SortedArray {
     export const Empty: SortedArray = Impl.Empty as any;
-    export const ofUnsortedArray: (xs: ArrayLike<number>) => SortedArray = Impl.ofUnsortedArray as any;
-    export const ofSingleton: (v: number) => SortedArray = Impl.ofSingleton as any;
-    export const ofSortedArray: (xs: ArrayLike<number>) => SortedArray = Impl.ofSortedArray as any;
+    export const ofUnsortedArray: <T extends number = number>(xs: ArrayLike<number>) => SortedArray<T> = Impl.ofUnsortedArray as any;
+    export const ofSingleton: <T extends number = number>(v: number) => SortedArray<T> = Impl.ofSingleton as any;
+    export const ofSortedArray: <T extends number = number>(xs: ArrayLike<number>) => SortedArray<T> = Impl.ofSortedArray as any;
     // create sorted array [min, max] (it DOES contain the max value)
-    export const ofRange: (min: number, max: number) => SortedArray = Impl.ofRange as any;
-    // create sorted array [min, max) (it DOES not contain the max value)
-    export const ofBounds: (min: number, max: number) => SortedArray = (min, max) => Impl.ofRange(min, max - 1) as any;
-    export const is: (v: any) => v is Interval = Impl.is as any;
-
-    export const has: (array: SortedArray, x: number) => boolean = Impl.has as any;
-    export const indexOf: (array: SortedArray, x: number) => number = Impl.indexOf as any;
-    export const indexOfInInterval: (array: SortedArray, x: number, bounds: Interval) => number = Impl.indexOfInInterval as any;
-
-    export const start: (array: SortedArray) => number = Impl.start as any;
-    export const end: (array: SortedArray) => number = Impl.end as any;
-    export const min: (array: SortedArray) => number = Impl.min as any;
-    export const max: (array: SortedArray) => number = Impl.max as any;
-    export const size: (array: SortedArray) => number = Impl.size as any;
-    export const hashCode: (array: SortedArray) => number = Impl.hashCode as any;
-
-    export const areEqual: (a: SortedArray, b: SortedArray) => boolean = Impl.areEqual as any;
-    export const areIntersecting: (a: SortedArray, b: SortedArray) => boolean = Impl.areIntersecting as any;
-    export const isSubset: (a: SortedArray, b: SortedArray) => boolean = Impl.isSubset as any;
-
-    export const union: (a: SortedArray, b: SortedArray) => SortedArray = Impl.union as any;
-    export const intersect: (a: SortedArray, b: SortedArray) => SortedArray = Impl.intersect as any;
-    export const subtract: (a: SortedArray, b: SortedArray) => SortedArray = Impl.subtract as any;
-
-    export const findPredecessorIndex: (array: SortedArray, x: number) => number = Impl.findPredecessorIndex as any;
-    export const findPredecessorIndexInInterval: (array: SortedArray, x: number, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
-    export const findRange: (array: SortedArray, min: number, max: number) => Interval = Impl.findRange as any;
-
-    export const deduplicate: (arrat: SortedArray) => SortedArray = Impl.deduplicate as any;
+    export const ofRange: <T extends number = number>(min: T, max: T) => SortedArray<T> = Impl.ofRange as any;
+    // create sorted array [min, max) (it does NOT contain the max value)
+    export const ofBounds: <T extends number = number>(min: T, max: T) => SortedArray<T> = (min, max) => Impl.ofRange(min, max - 1) as any;
+    export const is: <T extends number = number>(v: any) => v is SortedArray<T> = Impl.is as any;
+
+    export const has: <T extends number = number>(array: SortedArray<T>, x: T) => boolean = Impl.has as any;
+    export const indexOf: <T extends number = number>(array: SortedArray<T>, x: T) => number = Impl.indexOf as any;
+    export const indexOfInInterval: <T extends number = number>(array: SortedArray<T>, x: number, bounds: Interval) => number = Impl.indexOfInInterval as any;
+
+    // array[0]
+    export const start: <T extends number = number>(array: SortedArray<T>) => T = Impl.start as any;
+    // array[array.length - 1] + 1
+    export const end: <T extends number = number>(array: SortedArray<T>) => T = Impl.end as any;
+    export const min: <T extends number = number>(array: SortedArray<T>) => T = Impl.min as any;
+    export const max: <T extends number = number>(array: SortedArray<T>) => T = Impl.max as any;
+    export const size: <T extends number = number>(array: SortedArray<T>) => number = Impl.size as any;
+    export const hashCode: <T extends number = number>(array: SortedArray<T>) => number = Impl.hashCode as any;
+
+    export const areEqual: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => boolean = Impl.areEqual as any;
+    export const areIntersecting: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => boolean = Impl.areIntersecting as any;
+    export const isSubset: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => boolean = Impl.isSubset as any;
+
+    export const union: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => SortedArray<T> = Impl.union as any;
+    export const intersect: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => SortedArray<T> = Impl.intersect as any;
+    export const subtract: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => SortedArray<T> = Impl.subtract as any;
+
+    export const findPredecessorIndex: <T extends number = number>(array: SortedArray<T>, x: T) => number = Impl.findPredecessorIndex as any;
+    export const findPredecessorIndexInInterval: <T extends number = number>(array: SortedArray<T>, x: T, bounds: Interval) => number = Impl.findPredecessorIndexInInterval as any;
+    export const findRange: <T extends number = number>(array: SortedArray<T>, min: T, max: T) => Interval = Impl.findRange as any;
+    export const intersectionSize: <T extends number = number>(a: SortedArray<T>, b: SortedArray<T>) => number = Impl.intersectionSize as any;
+
+    export const deduplicate: <T extends number = number>(array: SortedArray<T>) => SortedArray<T> = Impl.deduplicate as any;
+    /** Returns indices of xs in the array. E.g. indicesOf([10, 11, 12], [10, 12]) ==> [0, 2] */
+    export const indicesOf: <T extends number = number, I extends number = number>(array: SortedArray<T>, xs: SortedArray<T>) => SortedArray<I> = Impl.indicesOf as any;
 }
 
-interface SortedArray extends ArrayLike<number> { '@type': 'int-sorted-array' }
+interface SortedArray<T extends number = number> extends ArrayLike<T> { '@type': 'int-sorted-array' }
 
 export default SortedArray

+ 86 - 0
src/mol-data/int/sorted-ranges.ts

@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Segmentation, OrderedSet, SortedArray, Interval } from '../int';
+import _Iterator from '../iterator';
+
+/** Pairs of min and max indices of sorted, non-overlapping ranges */
+type SortedRanges<T extends number = number> = SortedArray<T>
+
+namespace SortedRanges {
+    export function ofSortedRanges<T extends number = number>(array: ArrayLike<T>) { return SortedArray.ofSortedArray<T>(array) }
+    export function start<T extends number = number>(ranges: SortedRanges<T>) { return ranges[0] }
+    export function end<T extends number = number>(ranges: SortedRanges<T>) { return ranges[ranges.length - 1] + 1 }
+    export function min<T extends number = number>(ranges: SortedRanges<T>) { return ranges[0] }
+    export function max<T extends number = number>(ranges: SortedRanges<T>) { return ranges[ranges.length - 1] }
+    export function size<T extends number = number>(ranges: SortedRanges<T>) {
+        let size = 0
+        for(let i = 0, il = ranges.length; i < il; i += 2) {
+            size += ranges[i + 1] - ranges[i] + 1
+        }
+        return size
+    }
+
+    export function transientSegments<T extends number = number, I extends number = number>(ranges: SortedRanges<T>, set: OrderedSet<T>) {
+        return new Iterator<T, I>(ranges, set)
+    }
+
+    export class Iterator<T extends number = number, I extends number = number> implements _Iterator<Segmentation.Segment<I>> {
+        private value: Segmentation.Segment<I> = { index: 0 as I, start: 0 as T, end: 0 as T }
+
+        private curIndex = 0
+        private maxIndex = 0
+        private interval: Interval<T>
+        private curMin: T = 0 as T
+
+        hasNext: boolean = false;
+
+        updateInterval() {
+            this.interval = Interval.ofRange(this.ranges[this.curIndex], this.ranges[this.curIndex + 1])
+        }
+
+        updateValue() {
+            this.value.index = this.curIndex / 2 as I
+            this.value.start = OrderedSet.findPredecessorIndex(this.set, this.ranges[this.curIndex])
+            this.value.end = OrderedSet.findPredecessorIndex(this.set, this.ranges[this.curIndex + 1]) + 1
+        }
+
+        move() {
+            if (this.hasNext) {
+                this.updateValue()
+                while (this.curIndex <= this.maxIndex) {
+                    this.curIndex += 2
+                    this.curMin = Interval.end(this.interval)
+                    this.updateInterval()
+                    if (Interval.min(this.interval) >= this.curMin && OrderedSet.areIntersecting(this.interval, this.set)) break
+                }
+                this.hasNext = this.curIndex <= this.maxIndex
+            }
+            return this.value;
+        }
+
+        getRangeIndex(value: number) {
+            const index = SortedArray.findPredecessorIndex(this.ranges, value)
+            return (index % 2 === 1) ? index - 1 : index
+        }
+
+        constructor(private ranges: SortedRanges<T>, private set: OrderedSet<T>) {
+            // TODO cleanup, refactor to make it clearer
+            const min = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.min(set))
+            const max = SortedArray.findPredecessorIndex(this.ranges, OrderedSet.max(set))
+            if (ranges.length && min !== max) {
+                this.curIndex = this.getRangeIndex(OrderedSet.min(set))
+                this.maxIndex = Math.min(ranges.length - 2, this.getRangeIndex(OrderedSet.max(set)))
+                this.curMin = this.ranges[this.curIndex]
+                this.updateInterval()
+            }
+
+            this.hasNext = ranges.length > 0 && min !== max && this.curIndex <= this.maxIndex
+        }
+    }
+}
+
+export default SortedRanges

+ 1 - 0
src/mol-data/util.ts

@@ -5,6 +5,7 @@
  */
 
 export * from './util/chunked-array'
+export * from './util/buckets'
 export * from './util/equivalence-classes'
 export * from './util/hash-functions'
 export * from './util/sort'

+ 43 - 0
src/mol-data/util/_spec/buckets.spec.ts

@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { createRangeArray } from '../array';
+import { makeBuckets } from '../buckets';
+
+describe('buckets', () => {
+
+    function reorder(order: ArrayLike<number>, data: any[]): any[] {
+        const ret = [];
+        for (const i of (order as number[])) ret[ret.length] = data[i];
+        return ret;
+    }
+
+    it('full range', () => {
+        const xs = [1, 1, 2, 2, 3, 1];
+        const range = createRangeArray(0, xs.length - 1);
+        const bs = makeBuckets(range, i => xs[i]);
+
+        expect(reorder(range, xs)).toEqual([1, 1, 1, 2, 2, 3]);
+        expect(Array.from(bs)).toEqual([0, 3, 5, 6]);
+    });
+
+    it('sort', () => {
+        const xs = [3, 1, 2, 1, 2, 3];
+        const range = createRangeArray(0, xs.length - 1);
+        makeBuckets(range, i => xs[i], { sort: true });
+
+        expect(reorder(range, xs)).toEqual([1, 1, 2, 2, 3, 3]);
+    });
+
+    it('subrange', () => {
+        const xs = [2, 1, 2, 1, 2, 3, 1];
+        const range = createRangeArray(0, xs.length - 1);
+        const bs = makeBuckets(range, i => xs[i], { sort: false, start: 1, end: 5 });
+
+        expect(reorder(range, xs)).toEqual([2, 1, 1, 2, 2, 3, 1]);
+        expect(Array.from(bs)).toEqual([1, 3, 5]);
+    })
+})

+ 29 - 0
src/mol-data/util/_spec/combination.spec.ts

@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { combinations } from '../combination'
+
+describe('Combination', () => {
+    it('test123-2', () => {
+        const c = combinations([1, 2, 3], 2)
+        expect(c).toEqual([[1, 2], [1, 3], [2, 3]]);
+    });
+
+    it('test1234-2', () => {
+        const c = combinations([1, 2, 3, 4], 2)
+        expect(c).toEqual([[1, 2], [1, 3], [2, 3], [1, 4], [2, 4], [3, 4]]);
+    });
+
+    it('test1234-1', () => {
+        const c = combinations([1, 2, 3, 4], 1)
+        expect(c).toEqual([[1], [2], [3], [4]]);
+    });
+
+    it('test1234-4', () => {
+        const c = combinations([1, 2, 3, 4], 4)
+        expect(c).toEqual([[1, 2, 3, 4]]);
+    });
+});

+ 33 - 0
src/mol-data/util/_spec/interval-iterator.spec.ts

@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Interval, OrderedSet, SortedArray } from '../../int';
+import { IntervalIterator } from '../interval-iterator';
+
+ describe('interval', () => {
+    function testIterator(name: string, interval: Interval, set: OrderedSet, expectedValues: { index: number[], start: number[], end: number[]}) {
+        it(`iterator, ${name}`, () => {
+            const intervalIt = new IntervalIterator(interval, set)
+            const { index, start, end } = expectedValues
+    
+            let i = 0
+            while (intervalIt.hasNext) {
+                const segment = intervalIt.move()
+                expect(segment.index).toBe(index[i])
+                expect(segment.start).toBe(start[i])
+                expect(segment.end).toBe(end[i])
+                ++i
+            }
+            expect(i).toBe(index.length)
+        })
+    }
+
+    testIterator('basic',
+        Interval.ofRange(0, 5),
+        SortedArray.ofSortedArray([1, 3, 7, 8]),
+        { index: [1, 3], start: [0, 1], end: [1, 2] }
+    )
+});

+ 24 - 0
src/mol-data/util/array.ts

@@ -22,4 +22,28 @@ export function iterableToArray<T>(it: IterableIterator<T>): T[] {
         ret[ret.length] = value;
     }
     return ret;
+}
+
+/** Fills the array so that array[0] = start and array[array.length - 1] = end */
+export function createRangeArray(start: number, end: number, ctor?: Helpers.ArrayCtor<number>) {
+    const len = end - start + 1;
+    const array = ctor ? new ctor(len) : new Int32Array(len);
+    for (let i = 0; i < len; i++) {
+        array[i] = i + start;
+    }
+    return array;
+}
+
+export function arrayPickIndices<T>(array: ArrayLike<T>, indices: ArrayLike<number>) {
+    const ret = new (arrayGetCtor(array))(indices.length);
+    for (let i = 0, _i = indices.length; i < _i; i++) {
+        ret[i] = array[indices[i]];
+    }
+    return ret;
+}
+
+export function arrayGetCtor<T>(data: ArrayLike<T>): Helpers.ArrayCtor<T> {
+    const ret = (data as any).constructor;
+    if (!ret) throw new Error('data does not define a constructor and it should');
+    return ret;
 }

+ 104 - 0
src/mol-data/util/buckets.ts

@@ -0,0 +1,104 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { sort, arraySwap } from './sort';
+
+type Bucket = {
+    key: any,
+    count: number,
+    offset: number
+}
+
+function sortAsc(bs: Bucket[], i: number, j: number) { return bs[i].key < bs[j].key ? -1 : 1; }
+
+function _makeBuckets(indices: Helpers.ArrayLike<number>,
+    getKey: (i: number) => any, sortBuckets: boolean, start: number, end: number) {
+
+    const buckets = new Map<any, Bucket>();
+    const bucketList: Bucket[] = [];
+
+    let prevKey = getKey(indices[0]);
+    let isBucketed = true;
+    for (let i = start; i < end; i++) {
+        const key = getKey(indices[i]);
+        if (buckets.has(key)) {
+            buckets.get(key)!.count++;
+            if (prevKey !== key) isBucketed = false;
+        } else {
+            const bucket: Bucket = { key, count: 1, offset: i };
+            buckets.set(key, bucket);
+            bucketList[bucketList.length] = bucket;
+        }
+        prevKey = key;
+    }
+
+    const bucketOffsets = new Int32Array(bucketList.length + 1);
+    bucketOffsets[bucketList.length] = end;
+
+    let sorted = true;
+    if (sortBuckets) {
+        for (let i = 1, _i = bucketList.length; i < _i; i++) {
+            if (bucketList[i - 1].key > bucketList[i].key) {
+                sorted = false;
+                break;
+            }
+        }
+    }
+
+    if (isBucketed && sorted) {
+        for (let i = 0; i < bucketList.length; i++) bucketOffsets[i] = bucketList[i].offset;
+        return bucketOffsets;
+    }
+
+    if (sortBuckets && !sorted) {
+        sort(bucketList, 0, bucketList.length, sortAsc, arraySwap);
+    }
+
+    let offset = 0;
+    for (let i = 0; i < bucketList.length; i++) {
+        const b = bucketList[i];
+        b.offset = offset;
+        offset += b.count;
+    }
+
+    const reorderedIndices = new Int32Array(end - start);
+    for (let i = start; i < end; i++) {
+        const key = getKey(indices[i]);
+        const bucket = buckets.get(key)!;
+        reorderedIndices[bucket.offset++] = indices[i];
+    }
+
+    for (let i = 0, _i = reorderedIndices.length; i < _i; i++) {
+        indices[i + start] = reorderedIndices[i];
+    }
+
+    bucketOffsets[0] = start;
+    for (let i = 1; i < bucketList.length; i++) bucketOffsets[i] = bucketList[i - 1].offset + start;
+
+    return bucketOffsets;
+}
+
+export interface MakeBucketsOptions<K> {
+    // If specified, will be sorted
+    sort?: boolean,
+    // inclusive start indidex
+    start?: number,
+    // exclusive end index
+    end?: number
+}
+
+/**
+ * Reorders indices so that the same keys are next to each other, [start, end)
+ * Returns the offsets of buckets. So that [offsets[i], offsets[i + 1]) determines the range.
+ */
+export function makeBuckets<K extends string | number>(
+    indices: Helpers.ArrayLike<number>, getKey: (i: number) => K, options?: MakeBucketsOptions<K>): ArrayLike<number> {
+    const s = (options && options.start) || 0;
+    const e = (options && options.end) || indices.length;
+    if (e - s <= 0) throw new Error('Can only bucket non-empty collections.');
+
+    return _makeBuckets(indices, getKey, !!(options && options.sort), s, e);
+}

+ 65 - 0
src/mol-data/util/combination.ts

@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+// adpated from https://github.com/dankogai/js-combinatorics, MIT 2013-2016 Dan Kogai
+
+import Iterator from '../iterator'
+
+function P(m: number, n: number) {
+    let p = 1
+    while (n--) p *= m--
+    return p
+}
+
+function C(m: number, n: number) {
+    if (n > m) return 0
+    return P(m, n) / P(n, n)
+}
+
+function nextIndex(n: number) {
+    const smallest = n & -n
+    const ripple = n + smallest
+    const newSmallest = ripple & -ripple
+    const ones = ((newSmallest / smallest) >> 1) - 1
+    return ripple | ones
+};
+
+export class CombinationIterator<T> implements Iterator<T[]> {
+    private value: T[]
+    private index: number
+    private maxIndex: number
+
+    size: number
+    hasNext: boolean = false;
+
+    move() {
+        if (this.hasNext) {
+            let i = 0, j = 0, n = this.index
+            for (; n; n >>>= 1, i++) {
+                if (n & 1) this.value[j++] = this.array[i]
+            }
+            this.index = nextIndex(this.index)
+            this.hasNext = this.index < this.maxIndex
+        }
+        return this.value;
+    }
+
+    constructor(private array: T[], count: number) {
+        this.index = (1 << count) - 1
+        this.size = C(array.length, count)
+        this.maxIndex = 1 << array.length,
+
+        this.value = new Array(count)
+        this.hasNext = count > 0 && count <= array.length
+    }
+}
+
+export function combinations<T>(array: T[], count: number): T[][] {
+    const out: T[][] = []
+    const combinationIt = new CombinationIterator(array, count)
+    while (combinationIt.hasNext) out.push(combinationIt.move().slice())
+    return out
+}

+ 8 - 0
src/mol-data/util/hash-functions.ts

@@ -59,4 +59,12 @@ export function hashString(s: string) {
  */
 export function cantorPairing(a: number, b: number) {
     return (a + b) * (a + b + 1) / 2 + b;
+}
+
+/**
+ * A unique number for each sorted pair of integers
+ * Biggest representable pair is (67108863, 67108863) (limit imposed by Number.MAX_SAFE_INTEGER)
+ */
+export function sortedCantorPairing(a: number, b: number) {
+    return a < b ? cantorPairing(a, b) : cantorPairing(b, a);
 }

+ 45 - 0
src/mol-data/util/interval-iterator.ts

@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import Iterator from '../iterator'
+import { OrderedSet, Interval, Segmentation } from '../int';
+
+/** Emits a segment of length one for each element in the interval that is also in the set */
+export class IntervalIterator<I extends number = number> implements Iterator<Segmentation.Segment<I>> {
+    private value: Segmentation.Segment<I> = { index: 0 as I, start: 0, end: 0 }
+
+    private curIndex = 0
+    private maxIndex = 0
+
+    hasNext: boolean = false;
+
+    updateValue() {
+        this.value.index = this.curIndex as I
+        this.value.start = OrderedSet.findPredecessorIndex(this.set, Interval.getAt(this.interval, this.curIndex))
+        this.value.end = this.value.start + 1
+    }
+
+    move() {
+        if (this.hasNext) {
+            this.updateValue()
+            while (this.curIndex <= this.maxIndex) {
+                ++this.curIndex
+                if (OrderedSet.has(this.set, this.curIndex)) break
+            }
+            this.hasNext = this.curIndex <= this.maxIndex
+        }
+        return this.value;
+    }
+
+    constructor(private interval: Interval<I>, private set: OrderedSet<I>) {
+        if (Interval.size(interval)) {
+            this.curIndex = Interval.findPredecessorIndex(interval, OrderedSet.min(set))
+            this.maxIndex = Interval.findPredecessorIndex(interval, OrderedSet.max(set))
+        }
+
+        this.hasNext = OrderedSet.areIntersecting(this.interval, this.set)
+    }
+}

+ 45 - 77
src/mol-geo/primitive/box.ts

@@ -7,101 +7,69 @@
 // adapted from three.js, MIT License Copyright 2010-2018 three.js authors
 
 import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive } from './primitive';
 
 export const DefaultBoxProps = {
     width: 1,
     height: 1,
-    depth: 1,
-    widthSegments: 1,
-    heightSegments: 1,
-    depthSegments: 1
+    depth: 1
 }
 export type BoxProps = Partial<typeof DefaultBoxProps>
 
-export default function Box(props?: BoxProps) {
-    const { width, height, depth, widthSegments, heightSegments, depthSegments } = { ...DefaultBoxProps, ...props }
+const tmpVector = Vec3.zero();
+
+export function Box(props?: BoxProps): Primitive {
+    const { width, height, depth } = { ...DefaultBoxProps, ...props }
 
     // buffers
-    const indices: number[] = [];
-    const vertices: number[] = [];
-    const normals: number[] = [];
+    const vertices = new Float32Array(72);
+    const normals = new Float32Array(72);
+    const indices = new Uint32Array(36);
 
     // helper variables
-    let numberOfVertices = 0;
+    let vertexCount = 0;
 
     // build each side of the box geometry
-    buildPlane(2, 1, 0, -1, -1, depth, height, width, depthSegments, heightSegments); // px
-    buildPlane(2, 1, 0, 1, -1, depth, height, -width, depthSegments, heightSegments); // nx
-    buildPlane(0, 2, 1, 1, 1, width, depth, height, widthSegments, depthSegments); // py
-    buildPlane(0, 2, 1, 1, -1, width, depth, -height, widthSegments, depthSegments); // ny
-    buildPlane(0, 1, 2, 1, -1, width, height, depth, widthSegments, heightSegments); // pz
-    buildPlane(0, 1, 2, -1, -1, width, height, -depth, widthSegments, heightSegments); // nz
-
-    return {
-        vertices: new Float32Array(vertices),
-        normals: new Float32Array(normals),
-        indices: new Uint32Array(indices)
-    }
-
-    function buildPlane(u: number, v: number, w: number, udir: number, vdir: number, width: number, height: number, depth: number, gridX: number, gridY: number) {
-
-        const segmentWidth = width / gridX;
-        const segmentHeight = height / gridY;
+    buildPlane(2, 1, 0, -1, -1, depth, height, width); // px
+    buildPlane(2, 1, 0, 1, -1, depth, height, -width); // nx
+    buildPlane(0, 2, 1, 1, 1, width, depth, height); // py
+    buildPlane(0, 2, 1, 1, -1, width, depth, -height); // ny
+    buildPlane(0, 1, 2, 1, -1, width, height, depth); // pz
+    buildPlane(0, 1, 2, -1, -1, width, height, -depth); // nz
 
-        const widthHalf = width / 2;
-        const heightHalf = height / 2;
-        const depthHalf = depth / 2;
-
-        const gridX1 = gridX + 1;
-        const gridY1 = gridY + 1;
-
-        let vertexCounter = 0;
-
-        const vector = Vec3.zero();
+    return { vertices, normals, indices }
 
+    function buildPlane(u: number, v: number, w: number, udir: number, vdir: number, width: number, height: number, depth: number) {
         // generate vertices and normals
-        for (let iy = 0; iy < gridY1; ++iy) {
-            const y = iy * segmentHeight - heightHalf;
-            for (let ix = 0; ix < gridX1; ++ix) {
-                const x = ix * segmentWidth - widthHalf;
-
-                // set values to correct vector component
-                vector[ u ] = x * udir;
-                vector[ v ] = y * vdir;
-                vector[ w ] = depthHalf;
-
-                // now apply vector to vertex buffer
-                vertices.push(...vector);
-
-                // set values to correct vector component
-                vector[ u ] = 0;
-                vector[ v ] = 0;
-                vector[ w ] = depth > 0 ? 1 : -1;
-
-                // now apply vector to normal buffer
-                normals.push(...vector);
-
-                vertexCounter += 1;
-            }
-        }
-
-        // indices
-        // 1. you need three indices to draw a single face
-        // 2. a single segment consists of two faces
-        // 3. so we need to generate six (2*3) indices per segment
-        for (let iy = 0; iy < gridY; ++iy) {
-            for (let ix = 0; ix < gridX; ++ix) {
-                const a = numberOfVertices + ix + gridX1 * iy;
-                const b = numberOfVertices + ix + gridX1 * (iy + 1);
-                const c = numberOfVertices + (ix + 1) + gridX1 * (iy + 1);
-                const d = numberOfVertices + (ix + 1) + gridX1 * iy;
-
-                // faces
-                indices.push(a, b, d);
-                indices.push(b, c, d);
+        for (let iy = 0; iy < 2; ++iy) {
+            const y = iy * height - height / 2;
+            for (let ix = 0; ix < 2; ++ix) {
+                const x = ix * width - width / 2;
+
+                // set values to correct vector component and add to vertex buffer
+                tmpVector[u] = x * udir;
+                tmpVector[v] = y * vdir;
+                tmpVector[w] = depth / 2;
+                Vec3.toArray(tmpVector, vertices, vertexCount * 3);
+
+                // set values to correct vector component and add to normal buffer
+                tmpVector[u] = 0;
+                tmpVector[v] = 0;
+                tmpVector[w] = depth > 0 ? 1 : -1;
+                Vec3.toArray(tmpVector, normals, vertexCount * 3);
+
+                ++vertexCount;
             }
         }
 
-        numberOfVertices += vertexCounter;
+        // faces
+        const vc = vertexCount - 4
+        const iidx = (vc / 2) * 3
+        indices[iidx] = vc
+        indices[iidx + 1] = vc + 2
+        indices[iidx + 2] = vc + 1
+        indices[iidx + 3] = vc + 2
+        indices[iidx + 4] = vc + 3
+        indices[iidx + 5] = vc + 1
     }
 }

+ 7 - 7
src/mol-geo/primitive/cylinder.ts

@@ -7,6 +7,7 @@
 // adapted from three.js, MIT License Copyright 2010-2018 three.js authors
 
 import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive } from './primitive';
 
 export const DefaultCylinderProps = {
     radiusTop: 1,
@@ -14,14 +15,15 @@ export const DefaultCylinderProps = {
     height: 1,
     radialSegments: 8,
     heightSegments: 1,
-    openEnded: false,
+    topCap: false,
+    bottomCap: false,
     thetaStart: 0.0,
     thetaLength: Math.PI * 2
 }
 export type CylinderProps = Partial<typeof DefaultCylinderProps>
 
-export default function Cylinder(props?: CylinderProps) {
-    const { radiusTop, radiusBottom, height, radialSegments, heightSegments, openEnded, thetaStart, thetaLength } = { ...DefaultCylinderProps, ...props };
+export function Cylinder(props?: CylinderProps): Primitive {
+    const { radiusTop, radiusBottom, height, radialSegments, heightSegments, topCap, bottomCap, thetaStart, thetaLength } = { ...DefaultCylinderProps, ...props };
 
     // buffers
     const indices: number[] = [];
@@ -36,10 +38,8 @@ export default function Cylinder(props?: CylinderProps) {
     // generate geometry
     generateTorso();
 
-    if (openEnded === false) {
-        if (radiusTop > 0) generateCap(true);
-        if (radiusBottom > 0) generateCap(false);
-    }
+    if (topCap && radiusTop > 0) generateCap(true);
+    if (bottomCap && radiusBottom > 0) generateCap(false);
 
     return {
         vertices: new Float32Array(vertices),

+ 12 - 24
src/mol-geo/primitive/icosahedron.ts

@@ -4,35 +4,23 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-// adapted from three.js, MIT License Copyright 2010-2018 three.js authors
-
-import Polyhedron from './polyhedron'
+import { createPrimitive, Primitive } from './primitive';
 
 const t = ( 1 + Math.sqrt( 5 ) ) / 2;
 
-const vertices = [
-    - 1, t, 0, 	1, t, 0, 	- 1, - t, 0, 	1, - t, 0,
-     0, - 1, t, 	0, 1, t,	0, - 1, - t, 	0, 1, - t,
-     t, 0, - 1, 	t, 0, 1, 	- t, 0, - 1, 	- t, 0, 1
+const icosahedronVertices: ReadonlyArray<number> = [
+    -1, t, 0,   1, t, 0,  -1, -t, 0,   1, -t, 0,
+     0, -1, t,  0, 1, t,   0, -1, -t,  0, 1, -t,
+     t, 0, -1,  t, 0, 1,  -t, 0, -1,  -t, 0, 1
 ];
 
-const indices = [
-     0, 11, 5, 	0, 5, 1, 	0, 1, 7, 	0, 7, 10, 	0, 10, 11,
-     1, 5, 9, 	5, 11, 4,	11, 10, 2,	10, 7, 6,	7, 1, 8,
-     3, 9, 4, 	3, 4, 2,	3, 2, 6,	3, 6, 8,	3, 8, 9,
-     4, 9, 5, 	2, 4, 11,	6, 2, 10,	8, 6, 7,	9, 8, 1
+const icosahedronIndices: ReadonlyArray<number> = [
+    0, 11, 5,  0, 5, 1,    0, 1, 7,    0, 7, 10,  0, 10, 11,
+    1, 5, 9,   5, 11, 4,  11, 10, 2,  10, 7, 6,   7, 1, 8,
+    3, 9, 4,   3, 4, 2,    3, 2, 6,    3, 6, 8,   3, 8, 9,
+    4, 9, 5,   2, 4, 11,   6, 2, 10,   8, 6, 7,   9, 8, 1
 ];
 
-export function icosahedronVertexCount(detail: number) {
-    return 10 * Math.pow(Math.pow(2, detail), 2) + 2
-}
-
-export const DefaultIcosahedronProps = {
-    radius: 1,
-    detail: 0
-}
-export type IcosahedronProps = Partial<typeof DefaultIcosahedronProps>
+const icosahedron = createPrimitive(icosahedronVertices, icosahedronIndices)
 
-export default function Icosahedron(props?: IcosahedronProps) {
-    return Polyhedron(vertices, indices, { ...DefaultIcosahedronProps, ...props })
-}
+export function Icosahedron(): Primitive { return icosahedron }

+ 22 - 0
src/mol-geo/primitive/octahedron.ts

@@ -0,0 +1,22 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createPrimitive, Primitive } from './primitive';
+
+export const octahedronVertices: ReadonlyArray<number> = [
+    0.5, 0, 0,   -0.5, 0, 0,    0, 0.5, 0,
+    0, -0.5, 0,     0, 0, 0.5,  0, 0, -0.5
+];
+
+export const octahedronIndices: ReadonlyArray<number> = [
+    0, 2, 4,  0, 4, 3,  0, 3, 5,
+    0, 5, 2,  1, 2, 5,  1, 5, 3,
+    1, 3, 4,  1, 4, 2
+];
+
+const octahedron = createPrimitive(octahedronVertices, octahedronIndices)
+
+export function Octahedron(): Primitive { return octahedron }

+ 36 - 0
src/mol-geo/primitive/plane.ts

@@ -0,0 +1,36 @@
+import { Primitive } from './primitive';
+
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export const DefaultPlaneProps = {
+    width: 1,
+    height: 1
+}
+export type PlaneProps = Partial<typeof DefaultPlaneProps>
+
+export function Plane(props?: PlaneProps): Primitive {
+    const { width, height } = { ...DefaultPlaneProps, ...props }
+
+    return {
+        vertices: new Float32Array([
+            -width / 2, height / 2, 0,
+            width / 2, height / 2, 0,
+            -width / 2, -height / 2, 0,
+            width / 2, -height / 2, 0
+        ]),
+        normals: new Float32Array([
+            0, 0, 1,
+            0, 0, 1,
+            0, 0, 1,
+            0, 0, 1
+        ]),
+        indices: new Uint32Array([
+            0, 2, 1,
+            1, 2, 3
+        ])
+    }
+}

+ 23 - 0
src/mol-geo/primitive/polygon.ts

@@ -0,0 +1,23 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+/**
+ * Create points for a polygon:
+ * 3 for a triangle, 4 for a rectangle, 5 for a pentagon, 6 for a hexagon...
+ */
+export function polygon(sideCount: number, shift: boolean) {
+    const points = new Float32Array(sideCount * 2)
+    const radius = sideCount <= 4 ? Math.sqrt(2) / 2 : 0.6
+
+    const offset = shift ? 1 : 0
+
+    for (let i = 0, il = 2 * sideCount; i < il; i += 2) {
+        const c = (i + offset) / sideCount * Math.PI
+        points[i] = Math.cos(c) * radius
+        points[i + 1] = Math.sin(c) * radius
+    }
+    return points
+}

+ 2 - 16
src/mol-geo/primitive/polyhedron.ts

@@ -8,6 +8,7 @@
 
 import { Vec3 } from 'mol-math/linear-algebra'
 import { computeIndexedVertexNormals, appplyRadius } from '../util'
+import { Primitive } from './primitive';
 
 export const DefaultPolyhedronProps = {
     radius: 1,
@@ -15,7 +16,7 @@ export const DefaultPolyhedronProps = {
 }
 export type PolyhedronProps = Partial<typeof DefaultPolyhedronProps>
 
-export default function Polyhedron(_vertices: Helpers.NumberArray, _indices: Helpers.NumberArray, props?: PolyhedronProps) {
+export function Polyhedron(_vertices: ArrayLike<number>, _indices: ArrayLike<number>, props?: PolyhedronProps): Primitive {
     const { radius, detail } = { ...DefaultPolyhedronProps, ...props }
     const builder = createBuilder()
     const { vertices, indices } = builder
@@ -28,7 +29,6 @@ export default function Polyhedron(_vertices: Helpers.NumberArray, _indices: Hel
 
     const normals = new Float32Array(vertices.length);
     computeIndexedVertexNormals(vertices, indices, normals)
-    // this.normalizeNormals(); // smooth normals
 
     return {
         vertices: new Float32Array(vertices),
@@ -85,20 +85,6 @@ export default function Polyhedron(_vertices: Helpers.NumberArray, _indices: Hel
             }
         }
 
-        // // construct all of the faces
-        // for (let i = 0; i < cols; ++i) {
-        //     for (let j = 0; j < 2 * (cols - i) - 1; ++j) {
-        //         const k = Math.floor(j / 2)
-        //         if (j % 2 === 0) {
-        //             vertices.push(...v[i][k + 1], ...v[i + 1][k], ...v[i][k])
-        //         } else {
-        //             vertices.push(...v[i][k + 1], ...v[i + 1][k + 1], ...v[i + 1][k])
-        //         }
-        //         const l = vertices.length / 3
-        //         indices.push(l - 3, l - 2, l - 1)
-        //     }
-        // }
-
         // construct all of the faces
         for (let i = 0; i < cols; ++i) {
             for (let j = 0; j < 2 * (cols - i) - 1; ++j) {

+ 59 - 0
src/mol-geo/primitive/primitive.ts

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+
+export interface Primitive {
+    vertices: ArrayLike<number>
+    normals: ArrayLike<number>
+    indices: ArrayLike<number>
+}
+
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero()
+
+/** Create primitive with face normals from vertices and indices */
+export function createPrimitive(vertices: ArrayLike<number>, indices: ArrayLike<number>): Primitive {
+    const count = indices.length
+    const builder = PrimitiveBuilder(count / 3)
+
+    for (let i = 0; i < count; i += 3) {
+        Vec3.fromArray(a, vertices, indices[i] * 3)
+        Vec3.fromArray(b, vertices, indices[i + 1] * 3)
+        Vec3.fromArray(c, vertices, indices[i + 2] * 3)
+        builder.add(a, b, c)
+    }
+    return builder.getPrimitive()
+}
+
+export interface PrimitiveBuilder {
+    add(a: Vec3, b: Vec3, c: Vec3): void
+    getPrimitive(): Primitive
+}
+
+const vn = Vec3.zero()
+
+/** Builder to create primitive with face normals */
+export function PrimitiveBuilder(triangleCount: number): PrimitiveBuilder {
+    const vertices = new Float32Array(triangleCount * 3 * 3)
+    const normals = new Float32Array(triangleCount * 3 * 3)
+    const indices = new Uint32Array(triangleCount * 3)
+    let offset = 0
+
+    return {
+        add: (a: Vec3, b: Vec3, c: Vec3) => {
+            Vec3.toArray(a, vertices, offset)
+            Vec3.toArray(b, vertices, offset + 3)
+            Vec3.toArray(c, vertices, offset + 6)
+            Vec3.triangleNormal(vn, a, b, c)
+            for (let j = 0; j < 3; ++j) {
+                Vec3.toArray(vn, normals, offset + 3 * j)
+                indices[offset / 3 + j] = offset / 3 + j
+            }
+            offset += 9
+        },
+        getPrimitive: () => ({ vertices, normals, indices })
+    }
+}

+ 100 - 0
src/mol-geo/primitive/prism.ts

@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive, PrimitiveBuilder } from './primitive';
+import { polygon } from './polygon'
+
+const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5)
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
+
+/**
+ * Create a prism with a poligonal base
+ */
+export function Prism(points: ArrayLike<number>): Primitive {
+    const sideCount = points.length / 2
+    const baseCount = sideCount === 3 ? 1 : sideCount === 4 ? 2 : sideCount
+    const count = 2 * baseCount + 2 * sideCount
+    const builder = PrimitiveBuilder(count)
+
+    // create sides
+    for (let i = 0; i < sideCount; ++i) {
+        const ni = (i + 1) % sideCount
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+        Vec3.set(c, points[ni * 2], points[ni * 2 + 1], 0.5)
+        Vec3.set(d, points[i * 2], points[i * 2 + 1], 0.5)
+        builder.add(a, b, c)
+        builder.add(c, d, a)
+    }
+
+    // create bases
+    if (sideCount === 3) {
+        Vec3.set(a, points[0], points[1], -0.5)
+        Vec3.set(b, points[2], points[3], -0.5)
+        Vec3.set(c, points[4], points[5], -0.5)
+        builder.add(a, b, c)
+        Vec3.set(a, points[0], points[1], 0.5)
+        Vec3.set(b, points[2], points[3], 0.5)
+        Vec3.set(c, points[4], points[5], 0.5)
+        builder.add(c, b, a)
+    } else if (sideCount === 4) {
+        Vec3.set(a, points[0], points[1], -0.5)
+        Vec3.set(b, points[2], points[3], -0.5)
+        Vec3.set(c, points[4], points[5], -0.5)
+        Vec3.set(d, points[6], points[7], -0.5)
+        builder.add(a, b, c)
+        builder.add(c, d, a)
+        Vec3.set(a, points[0], points[1], 0.5)
+        Vec3.set(b, points[2], points[3], 0.5)
+        Vec3.set(c, points[4], points[5], 0.5)
+        Vec3.set(d, points[6], points[7], 0.5)
+        builder.add(a, b, c)
+        builder.add(c, d, a)
+    } else {
+        for (let i = 0; i < sideCount; ++i) {
+            const ni = (i + 1) % sideCount
+            Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+            Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+            builder.add(a, b, on)
+            Vec3.set(a, points[i * 2], points[i * 2 + 1], 0.5)
+            Vec3.set(b, points[ni * 2], points[ni * 2 + 1], 0.5)
+            builder.add(op, b, a)
+        }
+    }
+
+    return builder.getPrimitive()
+}
+
+let wedge: Primitive
+export function Wedge() {
+    if (!wedge) wedge = Prism(polygon(3, false))
+    return wedge
+}
+
+let box: Primitive
+export function Box() {
+    if (!box) box = Prism(polygon(4, true))
+    return box
+}
+
+let diamond: Primitive
+export function DiamondPrism() {
+    if (!diamond) diamond = Prism(polygon(4, false))
+    return diamond
+}
+
+let pentagonalPrism: Primitive
+export function PentagonalPrism() {
+    if (!pentagonalPrism) pentagonalPrism = Prism(polygon(5, false))
+    return pentagonalPrism
+}
+
+let hexagonalPrism: Primitive
+export function HexagonalPrism() {
+    if (!hexagonalPrism) hexagonalPrism = Prism(polygon(6, true))
+    return hexagonalPrism
+}

+ 60 - 0
src/mol-geo/primitive/pyramid.ts

@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive, PrimitiveBuilder } from './primitive';
+import { polygon } from './polygon'
+
+const on = Vec3.create(0, 0, -0.5), op = Vec3.create(0, 0, 0.5)
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
+
+/**
+ * Create a pyramide with a poligonal base
+ */
+export function Pyramide(points: ArrayLike<number>): Primitive {
+    const sideCount = points.length / 2
+    const baseCount = sideCount === 3 ? 1 : sideCount === 4 ? 2 : sideCount
+    const count = 2 * baseCount + 2 * sideCount
+    const builder = PrimitiveBuilder(count)
+
+    // create sides
+    for (let i = 0; i < sideCount; ++i) {
+        const ni = (i + 1) % sideCount
+        Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+        Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+        builder.add(a, b, op)
+    }
+
+    // create base
+    if (sideCount === 3) {
+        Vec3.set(a, points[0], points[1], -0.5)
+        Vec3.set(b, points[2], points[3], -0.5)
+        Vec3.set(c, points[4], points[5], -0.5)
+        builder.add(a, b, c)
+    } else if (sideCount === 4) {
+        Vec3.set(a, points[0], points[1], -0.5)
+        Vec3.set(b, points[2], points[3], -0.5)
+        Vec3.set(c, points[4], points[5], -0.5)
+        Vec3.set(d, points[6], points[7], -0.5)
+        builder.add(a, b, c)
+        builder.add(c, d, a)
+    } else {
+        for (let i = 0; i < sideCount; ++i) {
+            const ni = (i + 1) % sideCount
+            Vec3.set(a, points[i * 2], points[i * 2 + 1], -0.5)
+            Vec3.set(b, points[ni * 2], points[ni * 2 + 1], -0.5)
+            builder.add(a, b, on)
+        }
+    }
+
+    return builder.getPrimitive()
+}
+
+let octagonalPyramide: Primitive
+export function OctagonalPyramide() {
+    if (!octagonalPyramide) octagonalPyramide = Pyramide(polygon(8, true))
+    return octagonalPyramide
+}

+ 208 - 0
src/mol-geo/primitive/sheet.ts

@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { ChunkedArray } from 'mol-data/util';
+import { MeshBuilderState } from '../shape/mesh-builder';
+
+const tA = Vec3.zero()
+const tB = Vec3.zero()
+const tV = Vec3.zero()
+
+const horizontalVector = Vec3.zero()
+const verticalVector = Vec3.zero()
+const normalOffset = Vec3.zero()
+const positionVector = Vec3.zero()
+const normalVector = Vec3.zero()
+const torsionVector = Vec3.zero()
+
+const arrowVerticalVector = Vec3.zero()
+const p1 = Vec3.zero()
+const p2 = Vec3.zero()
+const p3 = Vec3.zero()
+const p4 = Vec3.zero()
+const p5 = Vec3.zero()
+const p6 = Vec3.zero()
+const p7 = Vec3.zero()
+const p8 = Vec3.zero()
+
+export function addSheet(controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, width: number, height: number, arrowHeight: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) {
+    const { vertices, normals, indices } = state
+
+    let vertexCount = vertices.elementCount
+    let offsetLength = 0
+
+    if (arrowHeight > 0) {
+        Vec3.fromArray(tA, controlPoints, 0)
+        Vec3.fromArray(tB, controlPoints, linearSegments * 3)
+        offsetLength = arrowHeight / Vec3.magnitude(Vec3.sub(tV, tB, tA))
+    }
+
+    for (let i = 0; i <= linearSegments; ++i) {
+        const actualHeight = arrowHeight === 0 ? height : arrowHeight * (1 - i / linearSegments);
+        const i3 = i * 3
+
+        Vec3.fromArray(verticalVector, normalVectors, i3)
+        Vec3.scale(verticalVector, verticalVector, actualHeight);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, i3)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        if (arrowHeight > 0) {
+            Vec3.fromArray(tA, normalVectors, i3)
+            Vec3.fromArray(tB, binormalVectors, i3)
+            Vec3.scale(normalOffset, Vec3.cross(normalOffset, tA, tB), offsetLength)
+        }
+
+        Vec3.fromArray(positionVector, controlPoints, i3)
+        Vec3.fromArray(normalVector, normalVectors, i3)
+        Vec3.fromArray(torsionVector, binormalVectors, i3)
+
+        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
+        Vec3.copy(tB, normalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        // Vec3.add(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.add(tB, Vec3.negate(tB, torsionVector), normalOffset)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        // Vec3.sub(tA, Vec3.sub(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.negate(tB, normalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        // Vec3.sub(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector) // reuse tA
+        Vec3.add(tB, torsionVector, normalOffset)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+
+        Vec3.add(tA, Vec3.add(tA, positionVector, horizontalVector), verticalVector)
+        ChunkedArray.add3(vertices, tA[0], tA[1], tA[2])
+        ChunkedArray.add3(normals, tB[0], tB[1], tB[2])
+    }
+
+    for (let i = 0; i < linearSegments; ++i) {
+        for (let j = 0; j < 4; j++) {
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i * 8 + 2 * j,
+                vertexCount + (i + 1) * 8 + 2 * j + 1,
+                vertexCount + i * 8 + 2 * j + 1
+            );
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i * 8 + 2 * j,
+                vertexCount + (i + 1) * 8 + 2 * j,
+                vertexCount + (i + 1) * 8 + 2 * j + 1
+            );
+        }
+    }
+
+    if (startCap) {
+        const offset = 0
+        vertexCount = vertices.elementCount
+
+        Vec3.fromArray(verticalVector, normalVectors, offset)
+        Vec3.scale(verticalVector, verticalVector, height);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, offset)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        Vec3.fromArray(positionVector, controlPoints, offset)
+
+        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
+        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
+
+        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+
+        Vec3.cross(normalVector, horizontalVector, verticalVector)
+
+        if (arrowHeight === 0) {
+            for (let i = 0; i < 4; ++i) {
+                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+            }
+
+            ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+            ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+        } else {
+            Vec3.fromArray(arrowVerticalVector, normalVectors, offset)
+            Vec3.scale(arrowVerticalVector, verticalVector, arrowHeight);
+
+            Vec3.add(p5, Vec3.add(p5, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.sub(p6, Vec3.add(p6, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.sub(p7, Vec3.sub(p7, positionVector, horizontalVector), arrowVerticalVector)
+            Vec3.add(p8, Vec3.sub(p8, positionVector, horizontalVector), arrowVerticalVector)
+
+            ChunkedArray.add3(vertices, p5[0], p5[1], p5[2])
+            ChunkedArray.add3(vertices, p6[0], p6[1], p6[2])
+            ChunkedArray.add3(vertices, p7[0], p7[1], p7[2])
+            ChunkedArray.add3(vertices, p8[0], p8[1], p8[2])
+
+            for (let i = 0; i < 8; ++i) {
+                ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+            }
+
+            ChunkedArray.add3(indices, vertexCount + 7, vertexCount, vertexCount + 4);
+            ChunkedArray.add3(indices, vertexCount + 7, vertexCount + 3, vertexCount);
+            ChunkedArray.add3(indices, vertexCount + 5, vertexCount + 1, vertexCount + 6);
+            ChunkedArray.add3(indices, vertexCount + 1, vertexCount + 2, vertexCount + 6);
+        }
+    }
+
+    if (endCap && arrowHeight === 0) {
+        const offset = linearSegments * 3
+        vertexCount = vertices.elementCount
+
+        Vec3.fromArray(verticalVector, normalVectors, offset)
+        Vec3.scale(verticalVector, verticalVector, height);
+
+        Vec3.fromArray(horizontalVector, binormalVectors, offset)
+        Vec3.scale(horizontalVector, horizontalVector, width);
+
+        Vec3.fromArray(positionVector, controlPoints, offset)
+
+        Vec3.add(p1, Vec3.add(p1, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p2, Vec3.add(p2, positionVector, horizontalVector), verticalVector)
+        Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalVector)
+        Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalVector)
+
+        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+
+        Vec3.cross(normalVector, horizontalVector, verticalVector)
+
+        for (let i = 0; i < 4; ++i) {
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
+        }
+
+        ChunkedArray.add3(indices, vertexCount + 2, vertexCount + 1, vertexCount);
+        ChunkedArray.add3(indices, vertexCount, vertexCount + 3, vertexCount + 2);
+    }
+
+    return (linearSegments + 1) * 8 + (startCap ? (arrowHeight === 0 ? 4 : 8) : 0) + (endCap && arrowHeight === 0 ? 4 : 0)
+}

+ 27 - 0
src/mol-geo/primitive/sphere.ts

@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Polyhedron } from './polyhedron'
+import { Icosahedron } from  './icosahedron'
+import { Primitive } from './primitive';
+
+const { vertices, indices } = Icosahedron()
+
+/** Calculate vertex count for subdived icosahedron */
+export function sphereVertexCount(detail: number) {
+    return 10 * Math.pow(Math.pow(2, detail), 2) + 2
+}
+
+export const DefaultSphereProps = {
+    radius: 1,
+    detail: 0
+}
+export type SphereProps = Partial<typeof DefaultSphereProps>
+
+/** Create sphere by subdividing an icosahedron */
+export function Sphere(props?: SphereProps): Primitive {
+    return Polyhedron(vertices, indices, { ...DefaultSphereProps, ...props })
+}

+ 55 - 0
src/mol-geo/primitive/star.ts

@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive, PrimitiveBuilder } from './primitive';
+
+export const DefaultStarProps = {
+    pointCount: 5,
+    outerRadius: 1,
+    innerRadius: 0.5,
+    thickness: 0.3
+}
+export type StarProps = Partial<typeof DefaultStarProps>
+
+const op = Vec3.zero(), on = Vec3.zero()
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero()
+
+export function Star(props?: StarProps): Primitive {
+    const { outerRadius, innerRadius, thickness, pointCount } = { ...DefaultStarProps, ...props }
+
+    const triangleCount = pointCount * 2 * 2
+    const builder = PrimitiveBuilder(triangleCount)
+
+    const innerPoints = new Float32Array(pointCount * 2)
+    const outerPoints = new Float32Array(pointCount * 2)
+
+    for (let i = 0; i < pointCount; ++i) {
+        const io = i * 2, ii = i * 2 + 1
+        const co = (io + 1) / pointCount * Math.PI, ci = (ii + 1) / pointCount * Math.PI
+        outerPoints[io] = Math.cos(co) * outerRadius
+        outerPoints[ii] = Math.sin(co) * outerRadius
+        innerPoints[io] = Math.cos(ci) * innerRadius
+        innerPoints[ii] = Math.sin(ci) * innerRadius
+    }
+
+    Vec3.set(op, 0, 0, thickness / 2)
+    Vec3.set(on, 0, 0, -thickness / 2)
+
+    for (let i = 0; i < pointCount; ++i) {
+        const ni = (i + 1) % pointCount
+        Vec3.set(a, outerPoints[i * 2], outerPoints[i * 2 + 1], 0)
+        Vec3.set(b, innerPoints[i * 2], innerPoints[i * 2 + 1], 0)
+        Vec3.set(c, outerPoints[ni * 2], outerPoints[ni * 2 + 1], 0)
+
+        builder.add(op, a, b)
+        builder.add(on, a, b)
+        builder.add(op, b, c)
+        builder.add(on, b, c)
+    }
+
+    return builder.getPrimitive()
+}

+ 154 - 0
src/mol-geo/primitive/tube.ts

@@ -0,0 +1,154 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { ChunkedArray } from 'mol-data/util';
+import { MeshBuilderState } from '../shape/mesh-builder';
+
+const normalVector = Vec3.zero()
+const binormalVector = Vec3.zero()
+const controlPoint = Vec3.zero()
+const tempPos = Vec3.zero()
+const a = Vec3.zero()
+const b = Vec3.zero()
+const u = Vec3.zero()
+const v = Vec3.zero()
+
+export function addTube(controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean, state: MeshBuilderState) {
+    const { vertices, normals, indices } = state
+
+    let vertexCount = vertices.elementCount
+    const di = 1 / linearSegments
+
+    for (let i = 0; i <= linearSegments; ++i) {
+        const i3 = i * 3
+        Vec3.fromArray(u, normalVectors, i3)
+        Vec3.fromArray(v, binormalVectors, i3)
+
+        const tt = di * i - 0.5;
+        const ff = 1 + (waveFactor - 1) * (Math.cos(2 * Math.PI * tt) + 1);
+        const w = ff * width, h = ff * height;
+
+        for (let j = 0; j < radialSegments; ++j) {
+            const t = 2 * Math.PI * j / radialSegments;
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                normalVector,
+                Vec3.scale(a, a, w * Math.cos(t)),
+                Vec3.scale(b, b, h * Math.sin(t))
+            )
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                binormalVector,
+                Vec3.scale(a, a, h * Math.cos(t)),
+                Vec3.scale(b, b, w * Math.sin(t))
+            )
+            Vec3.normalize(normalVector, normalVector)
+
+            Vec3.fromArray(tempPos, controlPoints, i3)
+            Vec3.add(tempPos, tempPos, binormalVector)
+
+            ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+        }
+    }
+
+    for (let i = 0; i < linearSegments; ++i) {
+        for (let j = 0; j < radialSegments; ++j) {
+            ChunkedArray.add3(
+                indices,
+                vertexCount + i * radialSegments + (j + 1) % radialSegments,
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
+                vertexCount + i * radialSegments + j
+            );
+            ChunkedArray.add3(
+                indices,
+                vertexCount + (i + 1) * radialSegments + (j + 1) % radialSegments,
+                vertexCount + (i + 1) * radialSegments + j,
+                vertexCount + i * radialSegments + j
+            );
+        }
+    }
+
+    if (startCap) {
+        const offset = 0
+        vertexCount = vertices.elementCount
+        Vec3.fromArray(u, normalVectors, offset)
+        Vec3.fromArray(v, binormalVectors, offset)
+        Vec3.fromArray(controlPoint, controlPoints, offset)
+        Vec3.cross(normalVector, u, v)
+
+        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+        for (let i = 0; i < radialSegments; ++i) {
+            const t = 2 * Math.PI * i / radialSegments;
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                tempPos,
+                Vec3.scale(a, a, height * Math.cos(t)),
+                Vec3.scale(b, b, width * Math.sin(t))
+            )
+            Vec3.add(tempPos, controlPoint, tempPos)
+
+            ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+            ChunkedArray.add3(
+                indices,
+                vertexCount,
+                vertexCount + i + 1,
+                vertexCount + (i + 1) % radialSegments + 1
+            );
+        }
+    }
+
+    if (endCap) {
+        const offset = linearSegments * 3
+        vertexCount = vertices.elementCount
+        Vec3.fromArray(u, normalVectors, offset)
+        Vec3.fromArray(v, binormalVectors, offset)
+        Vec3.fromArray(controlPoint, controlPoints, offset)
+        Vec3.cross(normalVector, u, v)
+
+        ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
+        ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+        for (let i = 0; i < radialSegments; ++i) {
+            const t = 2 * Math.PI * i / radialSegments;
+
+            Vec3.copy(a, u)
+            Vec3.copy(b, v)
+            Vec3.add(
+                tempPos,
+                Vec3.scale(a, a, height * Math.cos(t)),
+                Vec3.scale(b, b, width * Math.sin(t))
+            )
+            Vec3.add(tempPos, controlPoint, tempPos)
+
+            ChunkedArray.add3(vertices, tempPos[0], tempPos[1], tempPos[2]);
+            ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
+
+            if (i < radialSegments - 2) {
+                ChunkedArray.add3(
+                    indices,
+                    vertexCount + i + 1,
+                    vertexCount + (i + 1) % radialSegments + 1,
+                    vertexCount
+                );
+            }
+        }
+    }
+
+    return (linearSegments + 1) * radialSegments + (startCap ? radialSegments + 1 : 0) + (endCap ? radialSegments + 1 : 0)
+}

+ 122 - 0
src/mol-geo/primitive/wedge.ts

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra'
+import { Primitive } from './primitive';
+
+export const DefaultWedgeProps = {
+    width: 1,
+    height: 1,
+    depth: 1
+}
+export type WedgeProps = Partial<typeof DefaultWedgeProps>
+
+const _a = Vec3.create(0, 0.5, 0.5)
+const _b = Vec3.create(0.5, -0.5, 0.5)
+const _c = Vec3.create(-0.5, -0.5, 0.5)
+const _d = Vec3.create(0, 0.5, -0.5)
+const _e = Vec3.create(0.5, -0.5, -0.5)
+const _f = Vec3.create(-0.5, -0.5, -0.5)
+
+const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero()
+const d = Vec3.zero(), e = Vec3.zero(), f = Vec3.zero()
+
+const nabc = Vec3.create(0, 0, 1)
+const ndef = Vec3.create(0, 0, -1)
+const nabde = Vec3.zero()
+const nbcef = Vec3.create(0, -1, 0)
+const nacdf = Vec3.zero()
+
+const s = Vec3.zero()
+
+export function Wedge(props?: WedgeProps): Primitive {
+    const { width, height, depth } = { ...DefaultWedgeProps, ...props }
+
+    const vertices = new Float32Array(54)
+    const normals = new Float32Array(54)
+    const indices = new Uint32Array(24)
+
+    Vec3.set(s, width, height, depth)
+    Vec3.mul(a, _a, s); Vec3.mul(b, _b, s); Vec3.mul(c, _c, s)
+    Vec3.mul(d, _d, s); Vec3.mul(e, _e, s); Vec3.mul(f, _f, s)
+
+    Vec3.sub(nabde, b, a)
+    Vec3.normalize(nabde, Vec3.set(nabde, -nabde[1], nabde[0], 0))
+    Vec3.sub(nacdf, c, a)
+    Vec3.normalize(nacdf, Vec3.set(nacdf, nacdf[1], -nacdf[0], 0))
+
+    let vc = 0
+    let ic = 0
+
+    // abc
+    Vec3.toArray(a, vertices, vc + 0)
+    Vec3.toArray(c, vertices, vc + 3)
+    Vec3.toArray(b, vertices, vc + 6)
+    for (let i = 0; i < 3; ++i) Vec3.toArray(nabc, normals, vc + i * 3)
+    indices[ic + 0] = vc / 3 + 0
+    indices[ic + 1] = vc / 3 + 1
+    indices[ic + 2] = vc / 3 + 2
+    vc += 9
+    ic += 3
+
+    // def
+    Vec3.toArray(d, vertices, vc + 0)
+    Vec3.toArray(e, vertices, vc + 3)
+    Vec3.toArray(f, vertices, vc + 6)
+    for (let i = 0; i < 3; ++i) Vec3.toArray(ndef, normals, vc + i * 3)
+    indices[ic + 0] = vc / 3 + 0
+    indices[ic + 1] = vc / 3 + 1
+    indices[ic + 2] = vc / 3 + 2
+    vc += 9
+    ic += 3
+
+    // abde
+    Vec3.toArray(a, vertices, vc + 0)
+    Vec3.toArray(d, vertices, vc + 3)
+    Vec3.toArray(e, vertices, vc + 6)
+    Vec3.toArray(b, vertices, vc + 9)
+    for (let i = 0; i < 4; ++i) Vec3.toArray(nabde, normals, vc + i * 3)
+    indices[ic + 0] = vc / 3 + 2
+    indices[ic + 1] = vc / 3 + 1
+    indices[ic + 2] = vc / 3 + 0
+    indices[ic + 3] = vc / 3 + 0
+    indices[ic + 4] = vc / 3 + 3
+    indices[ic + 5] = vc / 3 + 2
+    vc += 12
+    ic += 6
+
+    // acdf
+    Vec3.toArray(d, vertices, vc + 0)
+    Vec3.toArray(a, vertices, vc + 3)
+    Vec3.toArray(c, vertices, vc + 6)
+    Vec3.toArray(f, vertices, vc + 9)
+    for (let i = 0; i < 4; ++i) Vec3.toArray(nacdf, normals, vc + i * 3)
+    indices[ic + 0] = vc / 3 + 2
+    indices[ic + 1] = vc / 3 + 1
+    indices[ic + 2] = vc / 3 + 0
+    indices[ic + 3] = vc / 3 + 0
+    indices[ic + 4] = vc / 3 + 3
+    indices[ic + 5] = vc / 3 + 2
+    vc += 12
+    ic += 6
+
+    // bcef
+    Vec3.toArray(e, vertices, vc + 0)
+    Vec3.toArray(f, vertices, vc + 3)
+    Vec3.toArray(c, vertices, vc + 6)
+    Vec3.toArray(b, vertices, vc + 9)
+    for (let i = 0; i < 4; ++i) Vec3.toArray(nbcef, normals, vc + i * 3)
+    indices[ic + 0] = vc / 3 + 2
+    indices[ic + 1] = vc / 3 + 1
+    indices[ic + 2] = vc / 3 + 0
+    indices[ic + 3] = vc / 3 + 0
+    indices[ic + 4] = vc / 3 + 3
+    indices[ic + 5] = vc / 3 + 2
+    vc += 12
+    ic += 6
+
+    return { vertices, normals, indices }
+}

+ 19 - 5
src/mol-geo/representation/index.ts

@@ -4,15 +4,29 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Task } from 'mol-task'
-import { RenderObject } from 'mol-gl/render-object';
-import { PickingId, PickingInfo } from '../util/picking';
+import { Task, RuntimeContext } from 'mol-task'
+import { RenderObject } from 'mol-gl/render-object'
+import { PickingId } from '../util/picking';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction } from '../util/marker-data';
 
 export interface RepresentationProps {}
 
 export interface Representation<D, P extends RepresentationProps = {}> {
-    renderObjects: ReadonlyArray<RenderObject>
+    readonly renderObjects: ReadonlyArray<RenderObject>
+    readonly props: Readonly<P>
     create: (data: D, props?: P) => Task<void>
     update: (props: P) => Task<void>
-    getLabel: (pickingId: PickingId) => PickingInfo | null
+    getLoci: (pickingId: PickingId) => Loci
+    mark: (loci: Loci, action: MarkerAction) => void
+    destroy: () => void
+}
+
+export interface Visual<D, P extends RepresentationProps = {}> {
+    readonly renderObject: RenderObject
+    create: (ctx: RuntimeContext, data: D, props: P) => Promise<void>
+    update: (ctx: RuntimeContext, props: P) => Promise<boolean>
+    getLoci: (pickingId: PickingId) => Loci
+    mark: (loci: Loci, action: MarkerAction) => void
+    destroy: () => void
 }

+ 52 - 0
src/mol-geo/representation/structure/backbone.ts

@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, StructureUnitsRepresentation } from '.';
+import { PickingId } from '../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { PolymerBackboneVisual, DefaultPolymerBackboneProps } from './visual/polymer-backbone-cylinder';
+
+export const DefaultBackboneProps = {
+    ...DefaultPolymerBackboneProps
+}
+export type BackboneProps = Partial<typeof DefaultBackboneProps>
+
+export function BackboneRepresentation(): StructureRepresentation<BackboneProps> {
+    const traceRepr = StructureUnitsRepresentation(PolymerBackboneVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...traceRepr.renderObjects ]
+        },
+        get props() {
+            return { ...traceRepr.props }
+        },
+        create: (structure: Structure, props: BackboneProps = {} as BackboneProps) => {
+            const p = Object.assign({}, DefaultBackboneProps, props)
+            return Task.create('BackboneRepresentation', async ctx => {
+                await traceRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: BackboneProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating BackboneRepresentation', async ctx => {
+                await traceRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            return traceRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            traceRepr.mark(loci, action)
+        },
+        destroy() {
+            traceRepr.destroy()
+        }
+    }
+}

+ 80 - 0
src/mol-geo/representation/structure/ball-and-stick.ts

@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, StructureUnitsRepresentation } from '.';
+import { ElementSphereVisual, DefaultElementSphereProps } from './visual/element-sphere';
+import { IntraUnitLinkVisual, DefaultIntraUnitLinkProps } from './visual/intra-unit-link-cylinder';
+import { PickingId } from '../../util/picking';
+import { Structure, Unit } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { SizeTheme } from '../../theme';
+import { InterUnitLinkVisual } from './visual/inter-unit-link-cylinder';
+
+export const DefaultBallAndStickProps = {
+    ...DefaultElementSphereProps,
+    ...DefaultIntraUnitLinkProps,
+
+    sizeTheme: { name: 'uniform', value: 0.25 } as SizeTheme,
+    unitKinds: [ Unit.Kind.Atomic ] as Unit.Kind[]
+}
+export type BallAndStickProps = Partial<typeof DefaultBallAndStickProps>
+
+export function BallAndStickRepresentation(): StructureRepresentation<BallAndStickProps> {
+    const elmementRepr = StructureUnitsRepresentation(ElementSphereVisual)
+    const intraLinkRepr = StructureUnitsRepresentation(IntraUnitLinkVisual)
+    const interLinkRepr = StructureRepresentation(InterUnitLinkVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...elmementRepr.renderObjects, ...intraLinkRepr.renderObjects, ...interLinkRepr.renderObjects ]
+        },
+        get props() {
+            return { ...elmementRepr.props, ...intraLinkRepr.props, ...interLinkRepr.props }
+        },
+        create: (structure: Structure, props: BallAndStickProps = {} as BallAndStickProps) => {
+            const p = Object.assign({}, DefaultBallAndStickProps, props)
+            return Task.create('DistanceRestraintRepresentation', async ctx => {
+                await elmementRepr.create(structure, p).runInContext(ctx)
+                await intraLinkRepr.create(structure, p).runInContext(ctx)
+                await interLinkRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: BallAndStickProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating BallAndStickRepresentation', async ctx => {
+                await elmementRepr.update(p).runInContext(ctx)
+                await intraLinkRepr.update(p).runInContext(ctx)
+                await interLinkRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            const sphereLoci = elmementRepr.getLoci(pickingId)
+            const intraLinkLoci = intraLinkRepr.getLoci(pickingId)
+            const interLinkLoci = interLinkRepr.getLoci(pickingId)
+            if (isEmptyLoci(sphereLoci)) {
+                if (isEmptyLoci(intraLinkLoci)) {
+                    return interLinkLoci
+                } else {
+                    return intraLinkLoci
+                }
+            } else {
+                return sphereLoci
+            }
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            elmementRepr.mark(loci, action)
+            intraLinkRepr.mark(loci, action)
+            interLinkRepr.mark(loci, action)
+        },
+        destroy() {
+            elmementRepr.destroy()
+            intraLinkRepr.destroy()
+            interLinkRepr.destroy()
+        }
+    }
+}

+ 88 - 0
src/mol-geo/representation/structure/cartoon.ts

@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation, StructureUnitsRepresentation } from '.';
+import { PickingId } from '../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { PolymerTraceVisual, DefaultPolymerTraceProps } from './visual/polymer-trace-mesh';
+import { PolymerGapVisual, DefaultPolymerGapProps } from './visual/polymer-gap-cylinder';
+import { NucleotideBlockVisual, DefaultNucleotideBlockProps } from './visual/nucleotide-block-mesh';
+import { PolymerDirectionVisual, DefaultPolymerDirectionProps } from './visual/polymer-direction-wedge';
+import { CarbohydrateSymbolVisual } from './visual/carbohydrate-symbol-mesh';
+
+export const DefaultCartoonProps = {
+    ...DefaultPolymerTraceProps,
+    ...DefaultPolymerGapProps,
+    ...DefaultNucleotideBlockProps,
+    ...DefaultPolymerDirectionProps
+}
+export type CartoonProps = Partial<typeof DefaultCartoonProps>
+
+export function CartoonRepresentation(): StructureRepresentation<CartoonProps> {
+    const traceRepr = StructureUnitsRepresentation(PolymerTraceVisual)
+    const gapRepr = StructureUnitsRepresentation(PolymerGapVisual)
+    const blockRepr = StructureUnitsRepresentation(NucleotideBlockVisual)
+    const directionRepr = StructureUnitsRepresentation(PolymerDirectionVisual)
+    const carbohydrateRepr = StructureRepresentation(CarbohydrateSymbolVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...traceRepr.renderObjects, ...gapRepr.renderObjects, ...blockRepr.renderObjects, ...directionRepr.renderObjects, ...carbohydrateRepr.renderObjects ]
+        },
+        get props() {
+            return { ...traceRepr.props, ...gapRepr.props, ...blockRepr.props, ...carbohydrateRepr.props }
+        },
+        create: (structure: Structure, props: CartoonProps = {} as CartoonProps) => {
+            const p = Object.assign({}, DefaultCartoonProps, props)
+            return Task.create('CartoonRepresentation', async ctx => {
+                await traceRepr.create(structure, p).runInContext(ctx)
+                await gapRepr.create(structure, p).runInContext(ctx)
+                await blockRepr.create(structure, p).runInContext(ctx)
+                await directionRepr.create(structure, p).runInContext(ctx)
+                await carbohydrateRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: CartoonProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating CartoonRepresentation', async ctx => {
+                await traceRepr.update(p).runInContext(ctx)
+                await gapRepr.update(p).runInContext(ctx)
+                await blockRepr.update(p).runInContext(ctx)
+                await directionRepr.update(p).runInContext(ctx)
+                await carbohydrateRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            const traceLoci = traceRepr.getLoci(pickingId)
+            const gapLoci = gapRepr.getLoci(pickingId)
+            const blockLoci = blockRepr.getLoci(pickingId)
+            const directionLoci = directionRepr.getLoci(pickingId)
+            const carbohydrateRepr = directionRepr.getLoci(pickingId)
+            return !isEmptyLoci(traceLoci) ? traceLoci
+                : !isEmptyLoci(gapLoci) ? gapLoci
+                : !isEmptyLoci(blockLoci) ? blockLoci
+                : !isEmptyLoci(directionLoci) ? directionLoci
+                : carbohydrateRepr
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            traceRepr.mark(loci, action)
+            gapRepr.mark(loci, action)
+            blockRepr.mark(loci, action)
+            directionRepr.mark(loci, action)
+            carbohydrateRepr.mark(loci, action)
+        },
+        destroy() {
+            traceRepr.destroy()
+            gapRepr.destroy()
+            blockRepr.destroy()
+            directionRepr.destroy()
+            carbohydrateRepr.destroy()
+        }
+    }
+}

+ 55 - 0
src/mol-geo/representation/structure/distance-restraint.ts

@@ -0,0 +1,55 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { StructureRepresentation } from '.';
+import { PickingId } from '../../util/picking';
+import { Structure } from 'mol-model/structure';
+import { Task } from 'mol-task';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { SizeTheme } from '../../theme';
+import { CrossLinkRestraintVisual, DefaultCrossLinkRestraintProps } from './visual/cross-link-restraint-cylinder';
+
+export const DefaultDistanceRestraintProps = {
+    ...DefaultCrossLinkRestraintProps,
+
+    sizeTheme: { name: 'uniform', value: 0.25 } as SizeTheme,
+}
+export type DistanceRestraintProps = Partial<typeof DefaultDistanceRestraintProps>
+
+export function DistanceRestraintRepresentation(): StructureRepresentation<DistanceRestraintProps> {
+    const crossLinkRepr = StructureRepresentation(CrossLinkRestraintVisual)
+
+    return {
+        get renderObjects() {
+            return [ ...crossLinkRepr.renderObjects ]
+        },
+        get props() {
+            return { ...crossLinkRepr.props }
+        },
+        create: (structure: Structure, props: DistanceRestraintProps = {} as DistanceRestraintProps) => {
+            const p = Object.assign({}, DefaultDistanceRestraintProps, props)
+            return Task.create('DistanceRestraintRepresentation', async ctx => {
+                await crossLinkRepr.create(structure, p).runInContext(ctx)
+            })
+        },
+        update: (props: DistanceRestraintProps) => {
+            const p = Object.assign({}, props)
+            return Task.create('Updating DistanceRestraintRepresentation', async ctx => {
+                await crossLinkRepr.update(p).runInContext(ctx)
+            })
+        },
+        getLoci: (pickingId: PickingId) => {
+            return crossLinkRepr.getLoci(pickingId)
+        },
+        mark: (loci: Loci, action: MarkerAction) => {
+            crossLinkRepr.mark(loci, action)
+        },
+        destroy() {
+            crossLinkRepr.destroy()
+        }
+    }
+}

+ 169 - 103
src/mol-geo/representation/structure/index.ts

@@ -5,126 +5,192 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { Structure, StructureSymmetry, Unit, Element, Queries } from 'mol-model/structure';
+import { Structure, Unit } from 'mol-model/structure';
 import { Task } from 'mol-task'
 import { RenderObject } from 'mol-gl/render-object';
-import { Representation, RepresentationProps } from '..';
-import { ColorTheme } from '../../theme';
-import { PickingId, PickingInfo } from '../../util/picking';
-
-export interface UnitsRepresentation<P> {
-    renderObjects: ReadonlyArray<RenderObject>
-    create: (group: Unit.SymmetryGroup, props: P) => Task<void>
-    update: (props: P) => Task<boolean>
-    getLocation: (pickingId: PickingId) => Element.Location | null
-}
-
-export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> {
-    renderObjects: ReadonlyArray<RenderObject>
-    create: (structure: Structure, props?: P) => Task<void>
-    update: (props: P) => Task<void>
-    getLocation: (pickingId: PickingId) => Element.Location | null
-    getLabel: (pickingId: PickingId) => PickingInfo | null
-}
+import { Representation, RepresentationProps, Visual } from '..';
+import { ColorTheme, SizeTheme } from '../../theme';
+import { PickingId } from '../../util/picking';
+import { Loci, EmptyLoci, isEmptyLoci } from 'mol-model/loci';
+import { MarkerAction } from '../../util/marker-data';
+import { getQualityProps, DefaultBaseProps } from '../util';
 
-interface GroupRepresentation<T> {
-    repr: UnitsRepresentation<T>
-    group: Unit.SymmetryGroup
-}
+export interface UnitsVisual<P extends RepresentationProps = {}> extends Visual<Unit.SymmetryGroup, P> { }
+export interface  StructureVisual<P extends RepresentationProps = {}> extends Visual<Structure, P> { }
 
-function label(loc: Element.Location) {
-    const model = loc.unit.model.label
-    const instance = loc.unit.conformation.operator.name
-    let element = ''
-
-    if (Unit.isAtomic(loc.unit)) {
-        const asym_id = Queries.props.chain.auth_asym_id(loc)
-        const seq_id = Queries.props.residue.auth_seq_id(loc)
-        const comp_id = Queries.props.residue.auth_comp_id(loc)
-        const atom_id = Queries.props.atom.auth_atom_id(loc)
-        element = `[${comp_id}]${seq_id}:${asym_id}.${atom_id}`
-    } else if (Unit.isCoarse(loc.unit)) {
-        const asym_id = Queries.props.coarse.asym_id(loc)
-        const seq_id_begin = Queries.props.coarse.seq_id_begin(loc)
-        const seq_id_end = Queries.props.coarse.seq_id_end(loc)
-        if (seq_id_begin === seq_id_end) {
-            const entityKey = Queries.props.coarse.entityKey(loc)
-            const seq = loc.unit.model.sequence.byEntityKey[entityKey]
-            const comp_id = seq.compId.value(seq_id_begin)
-            element = `[${comp_id}]${seq_id_begin}:${asym_id}`
-        } else {
-            element = `${seq_id_begin}-${seq_id_end}:${asym_id}`
-        }
-    } else {
-        element = 'unknown'
-    }
-
-    return { label: `${model} ${instance} ${element}` }
-}
+export interface StructureRepresentation<P extends RepresentationProps = {}> extends Representation<Structure, P> { }
 
 export const DefaultStructureProps = {
+    ...DefaultBaseProps,
     colorTheme: { name: 'instance-index' } as ColorTheme,
-    alpha: 1,
-    visible: true,
-    doubleSided: false,
-    depthMask: true,
-    hoverSelection: { objectId: -1, instanceId: -1, elementId: -1 } as PickingId
+    sizeTheme: { name: 'physical' } as SizeTheme,
 }
 export type StructureProps = Partial<typeof DefaultStructureProps>
 
-export function StructureRepresentation<P extends StructureProps>(reprCtor: () => UnitsRepresentation<P>): StructureRepresentation<P> {
-    const renderObjects: RenderObject[] = []
-    const groupReprs: GroupRepresentation<P>[] = []
-    // let currentProps: typeof DefaultStructureProps
-
-    function getLocation(pickingId: PickingId) {
-        for (let i = 0, il = groupReprs.length; i < il; ++i) {
-            const loc = groupReprs[i].repr.getLocation(pickingId)
-            if (loc) return loc
-        }
-        return null
+export function StructureRepresentation<P extends StructureProps>(visualCtor: () => StructureVisual<P>): StructureRepresentation<P> {
+    let visual: StructureVisual<P>
+
+    let _props: Required<P>
+    let _structure: Structure
+
+    function create(structure: Structure, props: P = {} as P) {
+        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
+
+        return Task.create('Creating StructureRepresentation', async ctx => {
+            if (!_structure) {
+                visual = visualCtor()
+                await visual.create(ctx, structure, _props)
+            } else {
+                if (_structure.hashCode === structure.hashCode) {
+                    await update(_props)
+                } else {
+                    if (!await visual.update(ctx, _props)) {
+                        await visual.create(ctx, _structure, _props)
+                    }
+                }
+            }
+            _structure = structure
+        });
+    }
+
+    function update(props: P) {
+        return Task.create('Updating StructureRepresentation', async ctx => {
+            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
+
+            if (!await visual.update(ctx, _props)) {
+                await visual.create(ctx, _structure, _props)
+            }
+        })
+    }
+
+    function getLoci(pickingId: PickingId) {
+        let loci: Loci = EmptyLoci
+        const _loci = visual.getLoci(pickingId)
+        if (!isEmptyLoci(_loci)) loci = _loci
+        return loci
+    }
+
+    function mark(loci: Loci, action: MarkerAction) {
+        visual.mark(loci, action)
+    }
+
+    function destroy() {
+        visual.destroy()
     }
 
     return {
-        renderObjects,
-        create(structure: Structure, props: P = {} as P) {
-            // currentProps = Object.assign({}, DefaultStructureProps, props)
-
-            return Task.create('StructureRepresentation.create', async ctx => {
-                // const { query } = currentProps
-                // const qs = await query(structure).runAsChild(ctx)
-                // const subStructure = Selection.unionStructure(qs)
-
-                const groups = StructureSymmetry.getTransformGroups(structure);
-                for (let i = 0; i < groups.length; i++) {
-                    const group = groups[i];
-                    const repr = reprCtor()
-                    groupReprs.push({ repr, group })
-                    await repr.create(group, props).runAsChild(ctx, { message: 'Building structure unit representations...', current: i, max: groups.length });
-                    renderObjects.push(...repr.renderObjects)
+        get renderObjects() { return [ visual.renderObject ] },
+        get props() { return _props },
+        create,
+        update,
+        getLoci,
+        mark,
+        destroy
+    }
+}
+
+export function StructureUnitsRepresentation<P extends StructureProps>(visualCtor: () => UnitsVisual<P>): StructureRepresentation<P> {
+    let visuals = new Map<number, { group: Unit.SymmetryGroup, visual: UnitsVisual<P> }>()
+
+    let _props: Required<P>
+    let _structure: Structure
+    let _groups: ReadonlyArray<Unit.SymmetryGroup>
+
+    function create(structure: Structure, props: P = {} as P) {
+        _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, structure))
+
+        return Task.create('Creating StructureRepresentation', async ctx => {
+            if (!_structure) {
+                _groups = structure.unitSymmetryGroups;
+                for (let i = 0; i < _groups.length; i++) {
+                    const group = _groups[i];
+                    const visual = visualCtor()
+                    await visual.create(ctx, group, _props)
+                    visuals.set(group.hashCode, { visual, group })
                 }
-            });
-        },
-        update(props: P) {
-            return Task.create('StructureRepresentation.update', async ctx => {
-                renderObjects.length = 0 // clear
-
-                for (let i = 0, il = groupReprs.length; i < il; ++i) {
-                    const groupRepr = groupReprs[i]
-                    const { repr, group } = groupRepr
-                    const state = { message: 'Updating structure unit representations...', current: i, max: il };
-                    if (!await repr.update(props).runAsChild(ctx, state)) {
-                        console.log('update failed, need to rebuild')
-                        await repr.create(group, props).runAsChild(ctx, state)
+            } else {
+                if (_structure.hashCode === structure.hashCode) {
+                    await update(_props)
+                } else {
+                    _groups = structure.unitSymmetryGroups;
+                    const newGroups: Unit.SymmetryGroup[] = []
+                    const oldUnitsVisuals = visuals
+                    visuals = new Map()
+                    for (let i = 0; i < _groups.length; i++) {
+                        const group = _groups[i];
+                        const visualGroup = oldUnitsVisuals.get(group.hashCode)
+                        if (visualGroup) {
+                            const { visual, group } = visualGroup
+                            if (!await visual.update(ctx, _props)) {
+                                await visual.create(ctx, group, _props)
+                            }
+                            oldUnitsVisuals.delete(group.hashCode)
+                        } else {
+                            newGroups.push(group)
+                            const visual = visualCtor()
+                            await visual.create(ctx, group, _props)
+                            visuals.set(group.hashCode, { visual, group })
+                        }
                     }
-                    renderObjects.push(...repr.renderObjects)
+
+                    // for new groups, reuse leftover visuals
+                    const unusedVisuals: UnitsVisual<P>[] = []
+                    oldUnitsVisuals.forEach(({ visual }) => unusedVisuals.push(visual))
+                    newGroups.forEach(async group => {
+                        const visual = unusedVisuals.pop() || visualCtor()
+                        await visual.create(ctx, group, _props)
+                        visuals.set(group.hashCode, { visual, group })
+                    })
+                    unusedVisuals.forEach(visual => visual.destroy())
+                }
+            }
+            _structure = structure
+        });
+    }
+
+    function update(props: P) {
+        return Task.create('Updating StructureRepresentation', async ctx => {
+            _props = Object.assign({}, DefaultStructureProps, _props, props, getQualityProps(props, _structure))
+
+            visuals.forEach(async ({ visual, group }) => {
+                if (!await visual.update(ctx, _props)) {
+                    await visual.create(ctx, group, _props)
                 }
             })
+        })
+    }
+
+    function getLoci(pickingId: PickingId) {
+        let loci: Loci = EmptyLoci
+        visuals.forEach(({ visual }) => {
+            const _loci = visual.getLoci(pickingId)
+            if (!isEmptyLoci(_loci)) loci = _loci
+        })
+        return loci
+    }
+
+    function mark(loci: Loci, action: MarkerAction) {
+        visuals.forEach(({ visual }) => visual.mark(loci, action))
+    }
+
+    function destroy() {
+        visuals.forEach(({ visual }) => visual.destroy())
+        visuals.clear()
+    }
+
+    return {
+        get renderObjects() {
+            const renderObjects: RenderObject[] = []
+            visuals.forEach(({ visual }) => renderObjects.push(visual.renderObject))
+            return renderObjects
+        },
+        get props() {
+            return _props
         },
-        getLocation,
-        getLabel(pickingId: PickingId) {
-            const loc = getLocation(pickingId)
-            return loc ? label(loc) : null
-        }
+        create,
+        update,
+        getLoci,
+        mark,
+        destroy
     }
 }

+ 0 - 169
src/mol-geo/representation/structure/point.ts

@@ -1,169 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { ValueCell } from 'mol-util/value-cell'
-import { createPointRenderObject, RenderObject, PointRenderObject } from 'mol-gl/render-object'
-import { Unit, Element } from 'mol-model/structure';
-import { Task } from 'mol-task'
-import { fillSerial } from 'mol-gl/renderable/util';
-
-import { UnitsRepresentation, DefaultStructureProps } from './index';
-import VertexMap from '../../shape/vertex-map';
-import { SizeTheme } from '../../theme';
-import { createTransforms, createColors, createSizes, createFlags } from './utils';
-import { deepEqual, defaults } from 'mol-util';
-import { SortedArray } from 'mol-data/int';
-import { RenderableState, PointValues } from 'mol-gl/renderable';
-import { PickingId } from '../../util/picking';
-
-export const DefaultPointProps = {
-    ...DefaultStructureProps,
-    sizeTheme: { name: 'vdw' } as SizeTheme
-}
-export type PointProps = Partial<typeof DefaultPointProps>
-
-export function createPointVertices(unit: Unit) {
-    const elements = unit.elements
-    const elementCount = elements.length
-    const vertices = new Float32Array(elementCount * 3)
-
-    const { x, y, z } = unit.conformation
-    const l = Element.Location()
-    l.unit = unit
-
-    for (let i = 0; i < elementCount; i++) {
-        l.element = elements[i];
-        const i3 = i * 3
-        vertices[i3] = x(l.element)
-        vertices[i3 + 1] = y(l.element)
-        vertices[i3 + 2] = z(l.element)
-    }
-    return vertices
-}
-
-export default function Point(): UnitsRepresentation<PointProps> {
-    const renderObjects: RenderObject[] = []
-    let points: PointRenderObject
-    let currentProps = DefaultPointProps
-    let currentGroup: Unit.SymmetryGroup
-
-    let _units: ReadonlyArray<Unit>
-    let _elements: SortedArray
-
-    return {
-        renderObjects,
-        create(group: Unit.SymmetryGroup, props: PointProps = {}) {
-            currentProps = Object.assign({}, DefaultPointProps, props)
-
-            return Task.create('Point.create', async ctx => {
-                renderObjects.length = 0 // clear
-                currentGroup = group
-
-                _units = group.units
-                _elements = group.elements;
-
-                const { colorTheme, sizeTheme, hoverSelection } = currentProps
-                const elementCount = _elements.length
-
-                const vertexMap = VertexMap.create(
-                    elementCount,
-                    elementCount + 1,
-                    fillSerial(new Uint32Array(elementCount)),
-                    fillSerial(new Uint32Array(elementCount + 1))
-                )
-
-                await ctx.update('Computing point vertices');
-                const vertices = createPointVertices(_units[0])
-
-                await ctx.update('Computing point transforms');
-                const transforms = createTransforms(group)
-
-                await ctx.update('Computing point colors');
-                const color = createColors(group, vertexMap, colorTheme)
-
-                await ctx.update('Computing point sizes');
-                const size = createSizes(group, vertexMap, sizeTheme)
-
-                await ctx.update('Computing spacefill flags');
-                const flag = createFlags(group, hoverSelection.instanceId, hoverSelection.elementId)
-
-                const instanceCount = group.units.length
-
-                const values: PointValues = {
-                    aPosition: ValueCell.create(vertices),
-                    aElementId: ValueCell.create(fillSerial(new Float32Array(elementCount))),
-                    aTransform: transforms,
-                    aInstanceId: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
-                    ...color,
-                    ...flag,
-                    ...size,
-
-                    uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
-                    uInstanceCount: ValueCell.create(instanceCount),
-                    uElementCount: ValueCell.create(group.elements.length),
-
-                    drawCount: ValueCell.create(vertices.length / 3),
-                    instanceCount: ValueCell.create(instanceCount),
-
-                    dPointSizeAttenuation: ValueCell.create(true)
-                }
-                const state: RenderableState = {
-                    depthMask: defaults(props.depthMask, true),
-                    visible: defaults(props.visible, true)
-                }
-
-                points = createPointRenderObject(values, state)
-                renderObjects.push(points)
-            })
-        },
-        update(props: PointProps) {
-            return Task.create('Point.update', async ctx => {
-                if (!points || !_units || !_elements) return false
-
-                const newProps = { ...currentProps, ...props }
-                if (deepEqual(currentProps, newProps)) {
-                    console.log('props identical, nothing to change')
-                    return true
-                }
-
-                // const elementCount = OrderedSet.size(_elementGroup.elements)
-                // const unitCount = _units.length
-
-                // const vertexMap = VertexMap.create(
-                //     elementCount,
-                //     elementCount + 1,
-                //     fillSerial(new Uint32Array(elementCount)),
-                //     fillSerial(new Uint32Array(elementCount + 1))
-                // )
-
-                if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) {
-                    console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme)
-                    // await ctx.update('Computing point colors');
-                    // const color = createColors(_units, _elementGroup, vertexMap, newProps.colorTheme)
-                    // ValueCell.update(points.props.color, color)
-                }
-
-                if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) {
-                    console.log('sizeTheme changed', currentProps.sizeTheme, newProps.sizeTheme)
-                }
-
-                currentProps = newProps
-                return false
-            })
-        },
-        getLocation(pickingId: PickingId) {
-            const { objectId, instanceId, elementId } = pickingId
-            if (points.id === objectId) {
-                const l = Element.Location()
-                l.unit = currentGroup.units[instanceId]
-                l.element = currentGroup.elements[elementId]
-                return l
-            }
-            return null
-        }
-    }
-}

+ 6 - 153
src/mol-geo/representation/structure/spacefill.ts

@@ -2,163 +2,16 @@
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
- * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { ValueCell } from 'mol-util/value-cell'
-
-import { RenderObject, createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
-import { Unit, Element, Queries } from 'mol-model/structure';
-import { UnitsRepresentation, DefaultStructureProps } from './index';
-import { Task } from 'mol-task'
-import { createTransforms, createColors, createFlags, createEmptyFlags, createSphereMesh } from './utils';
-import VertexMap from '../../shape/vertex-map';
-import { deepEqual, defaults } from 'mol-util';
-import { fillSerial } from 'mol-gl/renderable/util';
-import { RenderableState, MeshValues } from 'mol-gl/renderable';
-import { getMeshData } from '../../util/mesh-data';
-import { Mesh } from '../../shape/mesh';
-import { PickingId } from '../../util/picking';
-
-function createSpacefillMesh(unit: Unit, detail: number, mesh?: Mesh) {
-    let radius: Element.Property<number>
-    if (Unit.isAtomic(unit)) {
-        radius = Queries.props.atom.vdw_radius
-    } else if (Unit.isSpheres(unit)) {
-        radius = Queries.props.coarse.sphere_radius
-    } else {
-        console.warn('Unsupported unit type')
-        return Task.constant('Empty mesh', Mesh.createEmpty(mesh))
-    }
-    return createSphereMesh(unit, radius, detail, mesh)
-}
+import { StructureUnitsRepresentation } from '.';
+import { ElementSphereVisual, DefaultElementSphereProps } from './visual/element-sphere';
 
 export const DefaultSpacefillProps = {
-    ...DefaultStructureProps,
-    flipSided: false,
-    flatShaded: false,
-    detail: 0,
+    ...DefaultElementSphereProps,
 }
 export type SpacefillProps = Partial<typeof DefaultSpacefillProps>
 
-export default function Spacefill(): UnitsRepresentation<SpacefillProps> {
-    const renderObjects: RenderObject[] = []
-    let spheres: MeshRenderObject
-    let currentProps: typeof DefaultSpacefillProps
-    let mesh: Mesh
-    let currentGroup: Unit.SymmetryGroup
-    let vertexMap: VertexMap
-
-    return {
-        renderObjects,
-        create(group: Unit.SymmetryGroup, props: SpacefillProps = {}) {
-            currentProps = Object.assign({}, DefaultSpacefillProps, props)
-
-            return Task.create('Spacefill.create', async ctx => {
-                renderObjects.length = 0 // clear
-                currentGroup = group
-
-                const { detail, colorTheme, hoverSelection } = { ...DefaultSpacefillProps, ...props }
-
-                mesh = await createSpacefillMesh(group.units[0], detail).runAsChild(ctx, 'Computing spacefill mesh')
-                // console.log(mesh)
-                vertexMap = VertexMap.fromMesh(mesh)
-
-                await ctx.update('Computing spacefill transforms');
-                const transforms = createTransforms(group)
-
-                await ctx.update('Computing spacefill colors');
-                const color = createColors(group, vertexMap, colorTheme)
-
-                await ctx.update('Computing spacefill flags');
-                const flag = createFlags(group, hoverSelection.instanceId, hoverSelection.elementId)
-
-                const instanceCount = group.units.length
-
-                const values: MeshValues = {
-                    ...getMeshData(mesh),
-                    aTransform: transforms,
-                    aInstanceId: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
-                    ...color,
-                    ...flag,
-
-                    uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
-                    uInstanceCount: ValueCell.create(instanceCount),
-                    uElementCount: ValueCell.create(group.elements.length),
-
-                    elements: mesh.indexBuffer,
-
-                    drawCount: ValueCell.create(mesh.triangleCount * 3),
-                    instanceCount: ValueCell.create(instanceCount),
-
-                    dDoubleSided: ValueCell.create(defaults(props.doubleSided, true)),
-                    dFlatShaded: ValueCell.create(defaults(props.flatShaded, false)),
-                    dFlipSided: ValueCell.create(defaults(props.flipSided, false)),
-                }
-                const state: RenderableState = {
-                    depthMask: defaults(props.depthMask, true),
-                    visible: defaults(props.visible, true)
-                }
-
-                spheres = createMeshRenderObject(values, state)
-                renderObjects.push(spheres)
-            })
-        },
-        update(props: SpacefillProps) {
-            const newProps = Object.assign({}, currentProps, props)
-
-            return Task.create('Spacefill.update', async ctx => {
-                if (!spheres) return false
-
-                let updateColor = false
-
-                if (newProps.detail !== currentProps.detail) {
-                    mesh = await createSpacefillMesh(currentGroup.units[0], newProps.detail, mesh).runAsChild(ctx, 'Computing spacefill mesh')
-                    ValueCell.update(spheres.values.drawCount, mesh.triangleCount * 3)
-                    // TODO update in-place
-                    vertexMap = VertexMap.fromMesh(mesh)
-                    updateColor = true
-                }
-
-                if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
-                    updateColor = true
-                }
-
-                if (updateColor) {
-                    await ctx.update('Computing spacefill colors');
-                    createColors(currentGroup, vertexMap, newProps.colorTheme, spheres.values)
-                }
-
-                if (newProps.hoverSelection !== currentProps.hoverSelection) {
-                    await ctx.update('Computing spacefill flags');
-                    if (newProps.hoverSelection.objectId === spheres.id) {
-                        createFlags(currentGroup, newProps.hoverSelection.instanceId, newProps.hoverSelection.elementId, spheres.values)
-                    } else {
-                        createEmptyFlags(spheres.values)
-                    }
-                }
-
-                ValueCell.updateIfChanged(spheres.values.uAlpha, newProps.alpha)
-                ValueCell.updateIfChanged(spheres.values.dDoubleSided, newProps.doubleSided)
-                ValueCell.updateIfChanged(spheres.values.dFlipSided, newProps.flipSided)
-                ValueCell.updateIfChanged(spheres.values.dFlatShaded, newProps.flatShaded)
-
-                spheres.state.visible = newProps.visible
-                spheres.state.depthMask = newProps.depthMask
-
-                currentProps = newProps
-                return true
-            })
-        },
-        getLocation(pickingId: PickingId) {
-            const { objectId, instanceId, elementId } = pickingId
-            if (spheres.id === objectId) {
-                const l = Element.Location()
-                l.unit = currentGroup.units[instanceId]
-                l.element = currentGroup.elements[elementId]
-                return l
-            }
-            return null
-        }
-    }
-}
+export function SpacefillRepresentation() {
+    return StructureUnitsRepresentation(ElementSphereVisual)
+}

+ 0 - 133
src/mol-geo/representation/structure/utils.ts

@@ -1,133 +0,0 @@
-/**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- * @author David Sehnal <david.sehnal@gmail.com>
- */
-
-import { Unit, Element } from 'mol-model/structure';
-import { Mat4, Vec2, Vec3 } from 'mol-math/linear-algebra'
-
-import { createUniformColor, ColorData } from '../../util/color-data';
-import { createUniformSize } from '../../util/size-data';
-import { elementSizeData } from '../../theme/structure/size/element';
-import VertexMap from '../../shape/vertex-map';
-import { ColorTheme, SizeTheme } from '../../theme';
-import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdColorData } from '../../theme/structure/color';
-import { ValueCell } from 'mol-util';
-import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
-import { Mesh } from '../../shape/mesh';
-import { Task } from 'mol-task';
-import { icosahedronVertexCount } from '../../primitive/icosahedron';
-import { MeshBuilder } from '../../shape/mesh-builder';
-
-export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
-    const unitCount = units.length
-    const n = unitCount * 16
-    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
-    for (let i = 0; i < unitCount; i++) {
-        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
-    }
-    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
-}
-
-export function createColors(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: ColorTheme, colorData?: ColorData) {
-    switch (props.name) {
-        case 'atom-index':
-            return elementIndexColorData({ group, vertexMap }, colorData)
-        case 'chain-id':
-            return chainIdColorData({ group, vertexMap }, colorData)
-        case 'element-symbol':
-            return elementSymbolColorData({ group, vertexMap }, colorData)
-        case 'instance-index':
-            return instanceIndexColorData({ group, vertexMap }, colorData)
-        case 'uniform':
-            return createUniformColor(props, colorData)
-    }
-}
-
-export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: SizeTheme) {
-    switch (props.name) {
-        case 'uniform':
-            return createUniformSize(props)
-        case 'vdw':
-            return elementSizeData({ group, vertexMap })
-    }
-}
-
-export type FlagData = {
-    tFlag: ValueCell<TextureImage>
-    uFlagTexSize: ValueCell<Vec2>
-}
-
-export function createFlags(group: Unit.SymmetryGroup, instanceId: number, elementId: number, flagData?: FlagData): FlagData {
-    const instanceCount = group.units.length
-    const elementCount = group.elements.length
-    const count = instanceCount * elementCount
-    const flags = flagData && flagData.tFlag.ref.value.array.length >= count ? flagData.tFlag.ref.value : createTextureImage(count, 1)
-    let flagOffset = 0
-    for (let i = 0; i < instanceCount; i++) {
-        for (let j = 0, jl = elementCount; j < jl; ++j) {
-            flags.array[flagOffset] = (i === instanceId && j === elementId) ? 255 : 0
-            flagOffset += 1
-        }
-    }
-    // console.log(flags, instanceCount, elementCount)
-    if (flagData) {
-        ValueCell.update(flagData.tFlag, flags)
-        ValueCell.update(flagData.uFlagTexSize, Vec2.create(flags.width, flags.height))
-        return flagData
-    } else {
-        return {
-            tFlag: ValueCell.create(flags),
-            uFlagTexSize: ValueCell.create(Vec2.create(flags.width, flags.height)),
-        }
-    }
-}
-
-const emptyFlagTexture = { array: new Uint8Array(1), width: 1, height: 1 }
-export function createEmptyFlags(flagData?: FlagData) {
-    if (flagData) {
-        ValueCell.update(flagData.tFlag, emptyFlagTexture)
-        ValueCell.update(flagData.uFlagTexSize, Vec2.create(1, 1))
-        return flagData
-    } else {
-        return {
-            tFlag: ValueCell.create(emptyFlagTexture),
-            uFlagTexSize: ValueCell.create(Vec2.create(1, 1)),
-        }
-    }
-}
-
-export function createSphereMesh(unit: Unit, radius: Element.Property<number>, detail: number, mesh?: Mesh) {
-    return Task.create('Sphere mesh', async ctx => {
-        const { elements } = unit;
-        const elementCount = elements.length;
-        const vertexCount = elementCount * icosahedronVertexCount(detail)
-        const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
-
-        const v = Vec3.zero()
-        const m = Mat4.identity()
-
-        const { x, y, z } = unit.conformation
-        const l = Element.Location()
-        l.unit = unit
-
-        for (let i = 0; i < elementCount; i++) {
-            l.element = elements[i]
-            v[0] = x(l.element)
-            v[1] = y(l.element)
-            v[2] = z(l.element)
-            Mat4.setTranslation(m, v)
-
-            meshBuilder.setId(i)
-            meshBuilder.addIcosahedron(m, { radius: radius(l), detail })
-
-            if (i % 10000 === 0 && ctx.shouldUpdate) {
-                await ctx.update({ message: 'Sphere mesh', current: i, max: elementCount });
-            }
-        }
-
-        return meshBuilder.getMesh()
-    })
-}

+ 214 - 0
src/mol-geo/representation/structure/visual/carbohydrate-symbol-mesh.ts

@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, Structure } from 'mol-model/structure';
+import { DefaultStructureProps, StructureVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createIdentityTransform } from './util/common';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../../util/color-data';
+import { getSaccharideShape, SaccharideShapes } from 'mol-model/structure/structure/carbohydrates/constants';
+
+const t = Mat4.identity()
+const sVec = Vec3.zero()
+const p = Vec3.zero()
+const pd = Vec3.zero()
+
+async function createCarbohydrateSymbolMesh(ctx: RuntimeContext, structure: Structure, mesh?: Mesh) {
+    const builder = MeshBuilder.create(256, 128, mesh)
+
+    const carbohydrates = structure.carbohydrates
+
+    function centerAlign(center: Vec3, normal: Vec3, direction: Vec3) {
+        Vec3.add(pd, center, direction)
+        Mat4.targetTo(t, center, pd, normal)
+        Mat4.setTranslation(t, center)
+    }
+
+    const side = 1.75 * 2 * 0.806; // 0.806 == Math.cos(Math.PI / 4)
+    const radius = 1.75
+
+    const linkParams = { radiusTop: 0.4, radiusBottom: 0.4 }
+
+    for (let i = 0, il = carbohydrates.elements.length; i < il; ++i) {
+        const c = carbohydrates.elements[i];
+        if (!c.hasRing) continue;
+
+        const cGeo = c.geometry!
+        const shapeType = getSaccharideShape(c.component.type)
+
+        switch (shapeType) {
+            case SaccharideShapes.FilledSphere:
+                builder.addSphere(cGeo.center, radius, 2)
+                break;
+            case SaccharideShapes.FilledCube:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.scaleUniformly(t, t, side)
+                builder.addBox(t)
+                break;
+            case SaccharideShapes.CrossedCube:
+                // TODO split
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.scaleUniformly(t, t, side)
+                builder.addBox(t)
+                break;
+            case SaccharideShapes.FilledCone:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.scaleUniformly(t, t, side * 1.2)
+                builder.addOctagonalPyramid(t)
+                break
+            case SaccharideShapes.DevidedCone:
+                // TODO split
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.scaleUniformly(t, t, side * 1.2)
+                builder.addOctagonalPyramid(t)
+                break
+            case SaccharideShapes.FlatBox:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
+                builder.addBox(t)
+                break
+            case SaccharideShapes.FilledStar:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                builder.addStar(t, { outerRadius: side, innerRadius: side / 2, thickness: side / 2, pointCount: 5 })
+                break
+            case SaccharideShapes.FilledDiamond:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
+                builder.addOctahedron(t)
+                break
+            case SaccharideShapes.DividedDiamond:
+                // TODO split
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                Mat4.scale(t, t, Vec3.set(sVec, side * 1.4, side * 1.4, side * 1.4))
+                builder.addOctahedron(t)
+                break
+            case SaccharideShapes.FlatDiamond:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                Mat4.scale(t, t, Vec3.set(sVec, side, side / 2, side / 2))
+                builder.addDiamondPrism(t)
+                break
+            case SaccharideShapes.Pentagon:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZY90)
+                Mat4.scale(t, t, Vec3.set(sVec, side, side, side / 2))
+                builder.addPentagonalPrism(t)
+                break
+            case SaccharideShapes.FlatHexagon:
+            default:
+                centerAlign(cGeo.center, cGeo.normal, cGeo.direction)
+                Mat4.mul(t, t, Mat4.rotZYZ90)
+                Mat4.scale(t, t, Vec3.set(sVec, side / 1.5, side , side / 2))
+                builder.addHexagonalPrism(t)
+                break
+        }
+    }
+
+    for (let i = 0, il = carbohydrates.links.length; i < il; ++i) {
+        const l = carbohydrates.links[i]
+        const centerA = carbohydrates.elements[l.carbohydrateIndexA].geometry!.center
+        const centerB = carbohydrates.elements[l.carbohydrateIndexB].geometry!.center
+        builder.addCylinder(centerA, centerB, 0.5, linkParams)
+    }
+
+    for (let i = 0, il = carbohydrates.terminalLinks.length; i < il; ++i) {
+        const tl = carbohydrates.terminalLinks[i]
+        const center = carbohydrates.elements[tl.carbohydrateIndex].geometry!.center
+        tl.elementUnit.conformation.position(tl.elementUnit.elements[tl.elementIndex], p)
+        if (tl.fromCarbohydrate) {
+            builder.addCylinder(center, p, 0.5, linkParams)
+        } else {
+            builder.addCylinder(p, center, 0.5, linkParams)
+        }
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultCarbohydrateSymbolProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type CarbohydrateSymbolProps = Partial<typeof DefaultCarbohydrateSymbolProps>
+
+export function CarbohydrateSymbolVisual(): StructureVisual<CarbohydrateSymbolProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultCarbohydrateSymbolProps
+    let mesh: Mesh
+    let currentStructure: Structure
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, structure: Structure, props: CarbohydrateSymbolProps = {}) {
+            currentProps = Object.assign({}, DefaultCarbohydrateSymbolProps, props)
+            currentStructure = structure
+
+            const instanceCount = 1
+            const elementCount = currentStructure.elementCount
+
+            mesh = await createCarbohydrateSymbolMesh(ctx, currentStructure, mesh)
+            // console.log(mesh)
+
+            const transforms = createIdentityTransform()
+            const color = createUniformColor({ value: 0x999911 }) // TODO
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: CarbohydrateSymbolProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            return false
+        },
+        getLoci(pickingId: PickingId) {
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            // TODO
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 165 - 0
src/mol-geo/representation/structure/visual/cross-link-restraint-cylinder.ts

@@ -0,0 +1,165 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Link, Structure } from 'mol-model/structure';
+import { DefaultStructureProps, StructureVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../../util/color-data';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
+import { SizeTheme } from '../../../theme';
+import { createIdentityTransform } from './util/common';
+import { updateMeshValues, updateRenderableState, createMeshValues, createRenderableState } from '../../util';
+// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+
+async function createCrossLinkRestraintCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
+
+    const crossLinks = structure.crossLinkRestraints
+    if (!crossLinks.count) return Mesh.createEmpty(mesh)
+
+    const builderProps = {
+        linkCount: crossLinks.count,
+        referencePosition: (edgeIndex: number) => null,
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const b = crossLinks.pairs[edgeIndex]
+            // console.log(b)
+            const uA = b.unitA, uB = b.unitB
+            uA.conformation.position(uA.elements[b.indexA], posA)
+            uB.conformation.position(uB.elements[b.indexB], posB)
+            // console.log(posA, posB)
+        },
+        order: (edgeIndex: number) => 1,
+        flags: (edgeIndex: number) => 0
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const DefaultCrossLinkRestraintProps = {
+    ...DefaultStructureProps,
+    ...DefaultLinkCylinderProps,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+    flipSided: false,
+    flatShaded: false,
+}
+export type CrossLinkRestraintProps = Partial<typeof DefaultCrossLinkRestraintProps>
+
+export function CrossLinkRestraintVisual(): StructureVisual<CrossLinkRestraintProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultCrossLinkRestraintProps
+    let mesh: Mesh
+    let currentStructure: Structure
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, structure: Structure, props: CrossLinkRestraintProps = {}) {
+            currentProps = Object.assign({}, DefaultCrossLinkRestraintProps, props)
+            currentStructure = structure
+
+            const elementCount = structure.crossLinkRestraints.count
+            const instanceCount = 1
+
+            mesh = await createCrossLinkRestraintCylinderMesh(ctx, structure, currentProps)
+
+            const transforms = createIdentityTransform()
+            const color = createUniformColor({ value: 0x119911 }) // TODO
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: CrossLinkRestraintProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            // TODO create in-place
+            if (currentProps.radialSegments !== newProps.radialSegments) return false
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            return false
+        },
+        getLoci(pickingId: PickingId) {
+            return getLinkLoci(pickingId, currentStructure, renderObject.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markLink(loci, action, currentStructure, renderObject.values)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, elementId } = pickingId
+    if (id === objectId) {
+        const pair = structure.crossLinkRestraints.pairs[elementId]
+        if (pair) {
+            return Link.Loci([{
+                aUnit: pair.unitA,
+                aIndex: pair.indexA,
+                bUnit: pair.unitB,
+                bIndex: pair.indexB
+            }])
+        }
+    }
+    return EmptyLoci
+}
+
+function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) {
+    const tMarker = values.tMarker
+
+    const crossLinks = structure.crossLinkRestraints
+    const elementCount = crossLinks.count
+    const instanceCount = 1
+
+    let changed = false
+    const array = tMarker.ref.value.array
+    if (isEveryLoci(loci)) {
+        applyMarkerAction(array, 0, elementCount * instanceCount, action)
+        changed = true
+    } else if (Link.isLoci(loci)) {
+        for (const b of loci.links) {
+            const indices = crossLinks.getPairIndices(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
+            if (indices) {
+                for (let i = 0, il = indices.length; i < il; ++i) {
+                    const idx = indices[i]
+                    if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
+                        changed = true
+                    }
+                }
+            }
+        }
+    } else {
+        return
+    }
+    if (changed) {
+        ValueCell.update(tMarker, tMarker.ref.value)
+    }
+}

+ 141 - 0
src/mol-geo/representation/structure/visual/element-point.ts

@@ -0,0 +1,141 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+import { createPointRenderObject, PointRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { RuntimeContext } from 'mol-task'
+
+import { UnitsVisual, DefaultStructureProps } from '..';
+import VertexMap from '../../../shape/vertex-map';
+import { SizeTheme } from '../../../theme';
+import { markElement, getElementLoci } from './util/element';
+import { createTransforms, createColors, createSizes } from './util/common';
+import { deepEqual, defaults } from 'mol-util';
+import { SortedArray } from 'mol-data/int';
+import { RenderableState, PointValues } from 'mol-gl/renderable';
+import { PickingId } from '../../../util/picking';
+import { Loci } from 'mol-model/loci';
+import { MarkerAction, createMarkers } from '../../../util/marker-data';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { fillSerial } from 'mol-util/array';
+
+export const DefaultPointProps = {
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical' } as SizeTheme
+}
+export type PointProps = Partial<typeof DefaultPointProps>
+
+export function createPointVertices(unit: Unit) {
+    const elements = unit.elements
+    const elementCount = elements.length
+    const vertices = new Float32Array(elementCount * 3)
+
+    const pos = unit.conformation.invariantPosition
+
+    const p = Vec3.zero()
+    for (let i = 0; i < elementCount; i++) {
+        const i3 = i * 3
+        pos(elements[i], p)
+        vertices[i3] = p[0]
+        vertices[i3 + 1] = p[1]
+        vertices[i3 + 2] = p[2]
+    }
+    return vertices
+}
+
+export default function PointVisual(): UnitsVisual<PointProps> {
+    let renderObject: PointRenderObject
+    let currentProps = DefaultPointProps
+    let currentGroup: Unit.SymmetryGroup
+
+    let _units: ReadonlyArray<Unit>
+    let _elements: SortedArray
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PointProps = {}) {
+            currentProps = Object.assign({}, DefaultPointProps, props)
+            currentGroup = group
+
+            _units = group.units
+            _elements = group.elements;
+
+            const { colorTheme, sizeTheme } = currentProps
+            const elementCount = _elements.length
+            const instanceCount = group.units.length
+
+            const vertexMap = VertexMap.create(
+                elementCount,
+                elementCount + 1,
+                fillSerial(new Uint32Array(elementCount)),
+                fillSerial(new Uint32Array(elementCount + 1))
+            )
+
+            const vertices = createPointVertices(_units[0])
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const size = createSizes(group, vertexMap, sizeTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const values: PointValues = {
+                aPosition: ValueCell.create(vertices),
+                aElementId: ValueCell.create(fillSerial(new Float32Array(elementCount))),
+                aTransform: transforms,
+                aInstanceId: ValueCell.create(fillSerial(new Float32Array(instanceCount))),
+                ...color,
+                ...marker,
+                ...size,
+
+                uAlpha: ValueCell.create(defaults(props.alpha, 1.0)),
+                uInstanceCount: ValueCell.create(instanceCount),
+                uElementCount: ValueCell.create(group.elements.length),
+
+                drawCount: ValueCell.create(vertices.length / 3),
+                instanceCount: ValueCell.create(instanceCount),
+
+                dPointSizeAttenuation: ValueCell.create(true),
+                dUseFog: ValueCell.create(defaults(props.useFog, true)),
+            }
+            const state: RenderableState = {
+                depthMask: defaults(props.depthMask, true),
+                visible: defaults(props.visible, true)
+            }
+
+            renderObject = createPointRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PointProps) {
+            if (!renderObject || !_units || !_elements) return false
+
+            const newProps = { ...currentProps, ...props }
+            if (deepEqual(currentProps, newProps)) {
+                console.log('props identical, nothing to change')
+                return true
+            }
+
+            if (!deepEqual(currentProps.colorTheme, newProps.colorTheme)) {
+                console.log('colorTheme changed', currentProps.colorTheme, newProps.colorTheme)
+            }
+
+            if (!deepEqual(currentProps.sizeTheme, newProps.sizeTheme)) {
+                console.log('sizeTheme changed', currentProps.sizeTheme, newProps.sizeTheme)
+            }
+
+            currentProps = newProps
+            return false
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 116 - 0
src/mol-geo/representation/structure/visual/element-sphere.ts

@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { createElementSphereMesh, markElement, getElementRadius, getElementLoci } from './util/element';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+
+export const DefaultElementSphereProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type ElementSphereProps = Partial<typeof DefaultElementSphereProps>
+
+export function ElementSphereVisual(): UnitsVisual<ElementSphereProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultElementSphereProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: ElementSphereProps = {}) {
+            currentProps = Object.assign({}, DefaultElementSphereProps, props)
+            currentGroup = group
+
+            const { detail, colorTheme, sizeTheme, unitKinds } = { ...DefaultElementSphereProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            const radius = getElementRadius(unit, sizeTheme)
+            mesh = unitKinds.includes(unit.kind)
+                ? await createElementSphereMesh(ctx, unit, radius, detail, mesh)
+                : Mesh.createEmpty(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: ElementSphereProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                const radius = getElementRadius(unit, newProps.sizeTheme)
+                mesh = await createElementSphereMesh(ctx, unit, radius, newProps.detail, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing sphere colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 158 - 0
src/mol-geo/representation/structure/visual/inter-unit-link-cylinder.ts

@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Link, Structure } from 'mol-model/structure';
+import { DefaultStructureProps, StructureVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { LinkCylinderProps, DefaultLinkCylinderProps, createLinkCylinderMesh } from './util/link';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { createUniformColor } from '../../../util/color-data';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
+import { SizeTheme } from '../../../theme';
+import { createIdentityTransform } from './util/common';
+import { updateMeshValues, updateRenderableState, createMeshValues, createRenderableState } from '../../util';
+// import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+
+async function createInterUnitLinkCylinderMesh(ctx: RuntimeContext, structure: Structure, props: LinkCylinderProps, mesh?: Mesh) {
+    const links = structure.links
+    const { bondCount, bonds } = links
+
+    if (!bondCount) return Mesh.createEmpty(mesh)
+
+    const builderProps = {
+        linkCount: bondCount,
+        referencePosition: (edgeIndex: number) => null, // TODO
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            const b = bonds[edgeIndex]
+            const uA = b.unitA, uB = b.unitB
+            uA.conformation.position(uA.elements[b.indexA], posA)
+            uB.conformation.position(uB.elements[b.indexB], posB)
+        },
+        order: (edgeIndex: number) => bonds[edgeIndex].order,
+        flags: (edgeIndex: number) => bonds[edgeIndex].flag
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const DefaultInterUnitLinkProps = {
+    ...DefaultStructureProps,
+    ...DefaultLinkCylinderProps,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+}
+export type InterUnitLinkProps = Partial<typeof DefaultInterUnitLinkProps>
+
+export function InterUnitLinkVisual(): StructureVisual<InterUnitLinkProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultInterUnitLinkProps
+    let mesh: Mesh
+    let currentStructure: Structure
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, structure: Structure, props: InterUnitLinkProps = {}) {
+            currentProps = Object.assign({}, DefaultInterUnitLinkProps, props)
+            currentStructure = structure
+
+            const elementCount = structure.links.bondCount
+            const instanceCount = 1
+
+            mesh = await createInterUnitLinkCylinderMesh(ctx, structure, currentProps)
+
+            const transforms = createIdentityTransform()
+            const color = createUniformColor({ value: 0x999911 }) // TODO
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: InterUnitLinkProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            // TODO create in-place
+            if (currentProps.radialSegments !== newProps.radialSegments) return false
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            return false
+        },
+        getLoci(pickingId: PickingId) {
+            return getLinkLoci(pickingId, currentStructure, renderObject.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markLink(loci, action, currentStructure, renderObject.values)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}
+
+function getLinkLoci(pickingId: PickingId, structure: Structure, id: number) {
+    const { objectId, elementId } = pickingId
+    if (id === objectId) {
+        const bond = structure.links.bonds[elementId]
+        return Link.Loci([{
+            aUnit: bond.unitA,
+            aIndex: bond.indexA,
+            bUnit: bond.unitB,
+            bIndex: bond.indexB
+        }])
+    }
+    return EmptyLoci
+}
+
+function markLink(loci: Loci, action: MarkerAction, structure: Structure, values: MarkerData) {
+    const tMarker = values.tMarker
+
+    const links = structure.links
+    const elementCount = links.bondCount
+    const instanceCount = 1
+
+    let changed = false
+    const array = tMarker.ref.value.array
+    if (isEveryLoci(loci)) {
+        applyMarkerAction(array, 0, elementCount * instanceCount, action)
+        changed = true
+    } else if (Link.isLoci(loci)) {
+        for (const b of loci.links) {
+            const _idx = structure.links.getBondIndex(b.aIndex, b.aUnit, b.bIndex, b.bUnit)
+            if (_idx !== -1) {
+                const idx = _idx
+                if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
+                    changed = true
+                }
+            }
+        }
+    } else {
+        return
+    }
+    if (changed) {
+        ValueCell.update(tMarker, tMarker.ref.value)
+    }
+}

+ 179 - 0
src/mol-geo/representation/structure/visual/intra-unit-link-cylinder.ts

@@ -0,0 +1,179 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, Link } from 'mol-model/structure';
+import { UnitsVisual, DefaultStructureProps } from '..';
+import { RuntimeContext } from 'mol-task'
+import { DefaultLinkCylinderProps, LinkCylinderProps, createLinkCylinderMesh } from './util/link';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { Vec3 } from 'mol-math/linear-algebra';
+// import { createUniformColor } from '../../../util/color-data';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction, createMarkers, MarkerData } from '../../../util/marker-data';
+import { SizeTheme } from '../../../theme';
+import { chainIdLinkColorData } from '../../../theme/structure/color/chain-id';
+import { createTransforms } from './util/common';
+import { createMeshValues, createRenderableState, updateMeshValues, updateRenderableState } from '../../util';
+
+async function createIntraUnitLinkCylinderMesh(ctx: RuntimeContext, unit: Unit, props: LinkCylinderProps, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
+
+    const elements = unit.elements;
+    const links = unit.links
+    const { edgeCount, a, b, edgeProps, offset } = links
+    const { order: _order, flags: _flags } = edgeProps
+
+    if (!edgeCount) return Mesh.createEmpty(mesh)
+
+    const vRef = Vec3.zero()
+    const pos = unit.conformation.invariantPosition
+
+    const builderProps = {
+        linkCount: edgeCount * 2,
+        referencePosition: (edgeIndex: number) => {
+            let aI = a[edgeIndex], bI = b[edgeIndex];
+            if (aI > bI) [aI, bI] = [bI, aI]
+            for (let i = offset[aI], il = offset[aI + 1]; i < il; ++i) {
+                if (b[i] !== bI) return pos(elements[b[i]], vRef)
+            }
+            for (let i = offset[bI], il = offset[bI + 1]; i < il; ++i) {
+                if (a[i] !== aI) return pos(elements[a[i]], vRef)
+            }
+            return null
+        },
+        position: (posA: Vec3, posB: Vec3, edgeIndex: number) => {
+            pos(elements[a[edgeIndex]], posA)
+            pos(elements[b[edgeIndex]], posB)
+        },
+        order: (edgeIndex: number) => _order[edgeIndex],
+        flags: (edgeIndex: number) => _flags[edgeIndex]
+    }
+
+    return createLinkCylinderMesh(ctx, builderProps, props, mesh)
+}
+
+export const DefaultIntraUnitLinkProps = {
+    ...DefaultStructureProps,
+    ...DefaultLinkCylinderProps,
+    sizeTheme: { name: 'physical', factor: 0.3 } as SizeTheme,
+}
+export type IntraUnitLinkProps = Partial<typeof DefaultIntraUnitLinkProps>
+
+export function IntraUnitLinkVisual(): UnitsVisual<IntraUnitLinkProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultIntraUnitLinkProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: IntraUnitLinkProps = {}) {
+            currentProps = Object.assign({}, DefaultIntraUnitLinkProps, props)
+            currentGroup = group
+
+            const unit = group.units[0]
+            const elementCount = Unit.isAtomic(unit) ? unit.links.edgeCount * 2 : 0
+            const instanceCount = group.units.length
+
+            mesh = await createIntraUnitLinkCylinderMesh(ctx, unit, currentProps)
+
+            const transforms = createTransforms(group)
+            const color = chainIdLinkColorData({ group, elementCount }) // TODO
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: IntraUnitLinkProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            // TODO create in-place
+            if (currentProps.radialSegments !== newProps.radialSegments) return false
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getLinkLoci(pickingId, currentGroup, renderObject.id)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markLink(loci, action, currentGroup, renderObject.values)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}
+
+function getLinkLoci(pickingId: PickingId, group: Unit.SymmetryGroup, id: number) {
+    const { objectId, instanceId, elementId } = pickingId
+    const unit = group.units[instanceId]
+    if (id === objectId && Unit.isAtomic(unit)) {
+        return Link.Loci([{
+            aUnit: unit,
+            aIndex: unit.links.a[elementId],
+            bUnit: unit,
+            bIndex: unit.links.b[elementId]
+        }])
+    }
+    return EmptyLoci
+}
+
+function markLink(loci: Loci, action: MarkerAction, group: Unit.SymmetryGroup, values: MarkerData) {
+    const tMarker = values.tMarker
+    const unit = group.units[0]
+    if (!Unit.isAtomic(unit)) return
+
+    const elementCount = unit.links.edgeCount * 2
+    const instanceCount = group.units.length
+
+    let changed = false
+    const array = tMarker.ref.value.array
+    if (isEveryLoci(loci)) {
+        applyMarkerAction(array, 0, elementCount * instanceCount, action)
+        changed = true
+    } else if (Link.isLoci(loci)) {
+        for (const b of loci.links) {
+            const unitIdx = Unit.findUnitById(b.aUnit.id, group.units)
+            if (unitIdx !== -1) {
+                const _idx = unit.links.getDirectedEdgeIndex(b.aIndex, b.bIndex)
+                if (_idx !== -1) {
+                    const idx = _idx
+                    if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
+                        changed = true
+                    }
+                }
+            }
+        }
+    } else {
+        return
+    }
+    if (changed) {
+        ValueCell.update(tMarker, tMarker.ref.value)
+    }
+}

+ 206 - 0
src/mol-geo/representation/structure/visual/nucleotide-block-mesh.ts

@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getElementLoci, markElement } from './util/element';
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { Segmentation, SortedArray } from 'mol-data/int';
+import { MoleculeType, isNucleic, isPurinBase, isPyrimidineBase } from 'mol-model/structure/model/types';
+import { getElementIndexForAtomId, getElementIndexForAtomRole } from 'mol-model/structure/util';
+
+const p1 = Vec3.zero()
+const p2 = Vec3.zero()
+const p3 = Vec3.zero()
+const p4 = Vec3.zero()
+const p5 = Vec3.zero()
+const p6 = Vec3.zero()
+const v12 = Vec3.zero()
+const v34 = Vec3.zero()
+const vC = Vec3.zero()
+const center = Vec3.zero()
+const t = Mat4.identity()
+const sVec = Vec3.zero()
+
+async function createNucleotideBlockMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    if (!Unit.isAtomic(unit)) return Mesh.createEmpty(mesh)
+
+    const builder = MeshBuilder.create(256, 128, mesh)
+
+    const { elements, model } = unit
+    const { chemicalComponentMap, modifiedResidues } = model.properties
+    const { chainAtomSegments, residueAtomSegments, residues } = model.atomicHierarchy
+    const { label_comp_id } = residues
+    const pos = unit.conformation.invariantPosition
+
+    const chainIt = Segmentation.transientSegments(chainAtomSegments, elements)
+    const residueIt = Segmentation.transientSegments(residueAtomSegments, elements)
+
+    let i = 0
+    while (chainIt.hasNext) {
+        residueIt.setSegment(chainIt.move());
+
+        while (residueIt.hasNext) {
+            const { index: residueIndex } = residueIt.move();
+            const cc = chemicalComponentMap.get(label_comp_id.value(residueIndex))
+            const moleculeType = cc ? cc.moleculeType : MoleculeType.unknown
+
+            if (isNucleic(moleculeType)) {
+                let compId = label_comp_id.value(residueIndex)
+                const parentId = modifiedResidues.parentId.get(compId)
+                if (parentId !== undefined) compId = parentId
+                let idx1 = -1, idx2 = -1, idx3 = -1, idx4 = -1, idx5 = -1, idx6 = -1
+                let width = 4.5, height = 4.5, depth = 0.5
+
+                if (isPurinBase(compId)) {
+                    height = 4.5
+                    idx1 = getElementIndexForAtomId(model, residueIndex, 'N1')
+                    idx2 = getElementIndexForAtomId(model, residueIndex, 'C4')
+                    idx3 = getElementIndexForAtomId(model, residueIndex, 'C6')
+                    idx4 = getElementIndexForAtomId(model, residueIndex, 'C2')
+                    idx5 = getElementIndexForAtomId(model, residueIndex, 'N9')
+                    idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
+                } else if (isPyrimidineBase(compId)) {
+                    height = 3.0
+                    idx1 = getElementIndexForAtomId(model, residueIndex, 'N3')
+                    idx2 = getElementIndexForAtomId(model, residueIndex, 'C6')
+                    idx3 = getElementIndexForAtomId(model, residueIndex, 'C4')
+                    idx4 = getElementIndexForAtomId(model, residueIndex, 'C2')
+                    idx5 = getElementIndexForAtomId(model, residueIndex, 'N1')
+                    idx6 = getElementIndexForAtomRole(model, residueIndex, 'trace')
+                }
+
+                if (idx1 !== -1 && idx2 !== -1 && idx3 !== -1 && idx4 !== -1 && idx5 !== -1 && idx6 !== -1) {
+                    pos(idx1, p1); pos(idx2, p2); pos(idx3, p3); pos(idx4, p4); pos(idx5, p5); pos(idx6, p6)
+                    Vec3.normalize(v12, Vec3.sub(v12, p2, p1))
+                    Vec3.normalize(v34, Vec3.sub(v34, p4, p3))
+                    Vec3.normalize(vC, Vec3.cross(vC, v12, v34))
+                    Mat4.targetTo(t, p1, p2, vC)
+                    Vec3.scaleAndAdd(center, p1, v12, height / 2 - 0.2)
+                    Mat4.scale(t, t, Vec3.set(sVec, width, depth, height))
+                    Mat4.setTranslation(t, center)
+                    builder.setId(SortedArray.findPredecessorIndex(elements, idx6))
+                    builder.addBox(t)
+                    builder.addCylinder(p5, p6, 1, { radiusTop: 0.2, radiusBottom: 0.2 })
+                }
+            }
+
+            if (i % 10000 === 0 && ctx.shouldUpdate) {
+                await ctx.update({ message: 'Gap mesh', current: i });
+            }
+            ++i
+        }
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultNucleotideBlockProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type NucleotideBlockProps = Partial<typeof DefaultNucleotideBlockProps>
+
+export function NucleotideBlockVisual(): UnitsVisual<NucleotideBlockProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultNucleotideBlockProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: NucleotideBlockProps = {}) {
+            currentProps = Object.assign({}, DefaultNucleotideBlockProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultNucleotideBlockProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createNucleotideBlockMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+            // console.log(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: NucleotideBlockProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createNucleotideBlockMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing nucleotide block colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 152 - 0
src/mol-geo/representation/structure/visual/polymer-backbone-cylinder.ts

@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getPolymerElementCount, PolymerBackboneIterator } from './util/polymer';
+import { getElementLoci, markElement } from './util/element';
+import { Vec3 } from 'mol-math/linear-algebra';
+
+async function createPolymerBackboneCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerElementCount = getPolymerElementCount(unit)
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+    console.log('polymerElementCount backbone', polymerElementCount)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+
+    const { elements } = unit
+    const pos = unit.conformation.invariantPosition
+    const pA = Vec3.zero()
+    const pB = Vec3.zero()
+
+    let i = 0
+    const polymerBackboneIt = PolymerBackboneIterator(unit)
+    while (polymerBackboneIt.hasNext) {
+        // TODO size theme
+        const { centerA, centerB } = polymerBackboneIt.move()
+        pos(elements[centerA.element], pA)
+        pos(elements[centerB.element], pB)
+        builder.setId(centerA.element)
+        builder.addCylinder(pA, pB, 0.5, { radiusTop: 0.2, radiusBottom: 0.2 })
+        builder.setId(centerB.element)
+        builder.addCylinder(pB, pA, 0.5, { radiusTop: 0.2, radiusBottom: 0.2 })
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Backbone mesh', current: i, max: polymerElementCount });
+        }
+        ++i
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerBackboneProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerBackboneProps = Partial<typeof DefaultPolymerBackboneProps>
+
+export function PolymerBackboneVisual(): UnitsVisual<PolymerBackboneProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerBackboneProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerBackboneProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerBackboneProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerBackboneProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerBackboneCylinderMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+            // console.log(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerBackboneProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerBackboneCylinderMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 0 - 0
src/mol-geo/representation/structure/visual/polymer-backbone-sphere.ts


+ 184 - 0
src/mol-geo/representation/structure/visual/polymer-direction-wedge.ts

@@ -0,0 +1,184 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, StructureElement } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { markElement } from './util/element';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { OrderedSet } from 'mol-data/int';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { Vec3, Mat4 } from 'mol-math/linear-algebra';
+import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
+
+const t = Mat4.identity()
+const sVec = Vec3.zero()
+const n0 = Vec3.zero()
+const n1 = Vec3.zero()
+const upVec = Vec3.zero()
+
+async function createPolymerDirectionWedgeMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerElementCount = getPolymerElementCount(unit)
+    console.log('polymerElementCount direction', polymerElementCount)
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+    const linearSegments = 1
+
+    const state = createCurveSegmentState(linearSegments)
+    const { normalVectors, binormalVectors } = state
+
+    let i = 0
+    const polymerTraceIt = PolymerTraceIterator(unit)
+    while (polymerTraceIt.hasNext) {
+        const v = polymerTraceIt.move()
+        builder.setId(v.center.element)
+
+        const isNucleic = v.moleculeType === MoleculeType.DNA || v.moleculeType === MoleculeType.RNA
+        const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
+        const tension = (isNucleic || isSheet) ? 0.5 : 0.9
+
+        interpolateCurveSegment(state, v, tension)
+
+        if ((isSheet && !v.secStrucChange) || !isSheet) {
+
+            let width = 0.5, height = 1.2, depth = 0.6
+            if (isNucleic) {
+                Vec3.fromArray(n0, binormalVectors, 0)
+                Vec3.fromArray(n1, binormalVectors, 3)
+                Vec3.normalize(upVec, Vec3.add(upVec, n0, n1))
+                depth = 0.9
+            } else {
+                Vec3.fromArray(n0, normalVectors, 0)
+                Vec3.fromArray(n1, normalVectors, 3)
+                Vec3.normalize(upVec, Vec3.add(upVec, n0, n1))
+            }
+
+            Mat4.targetTo(t, v.p3, v.p1, upVec)
+            Mat4.mul(t, t, Mat4.rotY90)
+            Mat4.scale(t, t, Vec3.set(sVec, height, width, depth))
+            Mat4.setTranslation(t, v.p2)
+            builder.addWedge(t)
+        }
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Polymer direction mesh', current: i, max: polymerElementCount });
+        }
+        ++i
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerDirectionProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerDirectionProps = Partial<typeof DefaultPolymerDirectionProps>
+
+export function PolymerDirectionVisual(): UnitsVisual<PolymerDirectionProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerDirectionProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerDirectionProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerDirectionProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerDirectionProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerDirectionWedgeMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerDirectionProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerDirectionWedgeMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing direction colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            const { objectId, instanceId, elementId } = pickingId
+            if (renderObject.id === objectId) {
+                const unit = currentGroup.units[instanceId]
+                const indices = OrderedSet.ofSingleton(elementId as StructureElement.UnitIndex);
+                return StructureElement.Loci([{ unit, indices }])
+            }
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 158 - 0
src/mol-geo/representation/structure/visual/polymer-gap-cylinder.ts

@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getPolymerGapCount, PolymerGapIterator } from './util/polymer';
+import { getElementLoci, markElement } from './util/element';
+import { Vec3 } from 'mol-math/linear-algebra';
+
+async function createPolymerGapCylinderMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerGapCount = getPolymerGapCount(unit)
+    if (!polymerGapCount) return Mesh.createEmpty(mesh)
+    console.log('polymerGapCount', polymerGapCount)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerGapCount * 30, polymerGapCount * 30 / 2, mesh)
+
+    const { elements } = unit
+    const pos = unit.conformation.invariantPosition
+    const pA = Vec3.zero()
+    const pB = Vec3.zero()
+
+    let i = 0
+    const polymerGapIt = PolymerGapIterator(unit)
+    while (polymerGapIt.hasNext) {
+        // TODO size theme
+        const { centerA, centerB } = polymerGapIt.move()
+        if (centerA.element === centerB.element) {
+            builder.setId(centerA.element)
+            pos(elements[centerA.element], pA)
+            builder.addSphere(pA, 0.6, 0)
+        } else {
+            pos(elements[centerA.element], pA)
+            pos(elements[centerB.element], pB)
+            builder.setId(centerA.element)
+            builder.addFixedCountDashedCylinder(pA, pB, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+            builder.setId(centerB.element)
+            builder.addFixedCountDashedCylinder(pB, pA, 0.5, 10, { radiusTop: 0.2, radiusBottom: 0.2 })
+        }
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Gap mesh', current: i, max: polymerGapCount });
+        }
+        ++i
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerGapProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerGapProps = Partial<typeof DefaultPolymerGapProps>
+
+export function PolymerGapVisual(): UnitsVisual<PolymerGapProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerGapProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerGapProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerGapProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerGapProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerGapCylinderMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+            // console.log(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+                aColor: ValueCell.create(new Float32Array(mesh.vertexCount * 3))
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerGapProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerGapCylinderMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            return getElementLoci(renderObject.id, currentGroup, pickingId)
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 177 - 0
src/mol-geo/representation/structure/visual/polymer-trace-mesh.ts

@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { ValueCell } from 'mol-util/value-cell'
+
+import { createMeshRenderObject, MeshRenderObject } from 'mol-gl/render-object'
+import { Unit, StructureElement } from 'mol-model/structure';
+import { DefaultStructureProps, UnitsVisual } from '..';
+import { RuntimeContext } from 'mol-task'
+import { createTransforms, createColors } from './util/common';
+import { markElement } from './util/element';
+import { deepEqual } from 'mol-util';
+import { MeshValues } from 'mol-gl/renderable';
+import { getMeshData } from '../../../util/mesh-data';
+import { Mesh } from '../../../shape/mesh';
+import { PickingId } from '../../../util/picking';
+import { OrderedSet } from 'mol-data/int';
+import { createMarkers, MarkerAction } from '../../../util/marker-data';
+import { Loci, EmptyLoci } from 'mol-model/loci';
+import { SizeTheme } from '../../../theme';
+import { createMeshValues, updateMeshValues, updateRenderableState, createRenderableState, DefaultMeshProps } from '../../util';
+import { MeshBuilder } from '../../../shape/mesh-builder';
+import { getPolymerElementCount, PolymerTraceIterator, createCurveSegmentState, interpolateCurveSegment } from './util/polymer';
+import { SecondaryStructureType, MoleculeType } from 'mol-model/structure/model/types';
+
+// TODO handle polymer ends properly
+
+async function createPolymerTraceMesh(ctx: RuntimeContext, unit: Unit, mesh?: Mesh) {
+    const polymerElementCount = getPolymerElementCount(unit)
+    console.log('polymerElementCount trace', polymerElementCount)
+    if (!polymerElementCount) return Mesh.createEmpty(mesh)
+
+    // TODO better vertex count estimates
+    const builder = MeshBuilder.create(polymerElementCount * 30, polymerElementCount * 30 / 2, mesh)
+    const linearSegments = 8
+    const radialSegments = 12
+
+    const state = createCurveSegmentState(linearSegments)
+    const { curvePoints, normalVectors, binormalVectors } = state
+
+    let i = 0
+    const polymerTraceIt = PolymerTraceIterator(unit)
+    while (polymerTraceIt.hasNext) {
+        const v = polymerTraceIt.move()
+        builder.setId(v.center.element)
+
+        const isNucleic = v.moleculeType === MoleculeType.DNA || v.moleculeType === MoleculeType.RNA
+        const isSheet = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Beta)
+        const isHelix = SecondaryStructureType.is(v.secStrucType, SecondaryStructureType.Flag.Helix)
+        const tension = (isNucleic || isSheet) ? 0.5 : 0.9
+
+        // console.log('ELEMENT', i)
+        interpolateCurveSegment(state, v, tension)
+
+        let width = 0.2, height = 0.2
+
+        // TODO size theme
+        if (isSheet) {
+            width = 0.15; height = 1.0
+            const arrowHeight = v.secStrucChange ? 1.7 : 0
+            builder.addSheet(curvePoints, normalVectors, binormalVectors, linearSegments, width, height, arrowHeight, true, true)
+        } else {
+            if (isHelix) {
+                width = 0.2; height = 1.0
+            } else if (isNucleic) {
+                width = 1.5; height = 0.3
+            }
+            builder.addTube(curvePoints, normalVectors, binormalVectors, linearSegments, radialSegments, width, height, 1, true, true)
+        }
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Polymer trace mesh', current: i, max: polymerElementCount });
+        }
+        ++i
+    }
+
+    return builder.getMesh()
+}
+
+export const DefaultPolymerTraceProps = {
+    ...DefaultMeshProps,
+    ...DefaultStructureProps,
+    sizeTheme: { name: 'physical', factor: 1 } as SizeTheme,
+    detail: 0,
+    unitKinds: [ Unit.Kind.Atomic, Unit.Kind.Spheres ] as Unit.Kind[]
+}
+export type PolymerTraceProps = Partial<typeof DefaultPolymerTraceProps>
+
+export function PolymerTraceVisual(): UnitsVisual<PolymerTraceProps> {
+    let renderObject: MeshRenderObject
+    let currentProps: typeof DefaultPolymerTraceProps
+    let mesh: Mesh
+    let currentGroup: Unit.SymmetryGroup
+
+    return {
+        get renderObject () { return renderObject },
+        async create(ctx: RuntimeContext, group: Unit.SymmetryGroup, props: PolymerTraceProps = {}) {
+            currentProps = Object.assign({}, DefaultPolymerTraceProps, props)
+            currentGroup = group
+
+            const { colorTheme, unitKinds } = { ...DefaultPolymerTraceProps, ...props }
+            const instanceCount = group.units.length
+            const elementCount = group.elements.length
+            const unit = group.units[0]
+
+            mesh = unitKinds.includes(unit.kind)
+                ? await createPolymerTraceMesh(ctx, unit, mesh)
+                : Mesh.createEmpty(mesh)
+
+            const transforms = createTransforms(group)
+            const color = createColors(group, elementCount, colorTheme)
+            const marker = createMarkers(instanceCount * elementCount)
+
+            const counts = { drawCount: mesh.triangleCount * 3, elementCount, instanceCount }
+
+            const values: MeshValues = {
+                ...getMeshData(mesh),
+                ...color,
+                ...marker,
+                aTransform: transforms,
+                elements: mesh.indexBuffer,
+                ...createMeshValues(currentProps, counts),
+            }
+            const state = createRenderableState(currentProps)
+
+            renderObject = createMeshRenderObject(values, state)
+        },
+        async update(ctx: RuntimeContext, props: PolymerTraceProps) {
+            const newProps = Object.assign({}, currentProps, props)
+
+            if (!renderObject) return false
+
+            let updateColor = false
+
+            if (newProps.detail !== currentProps.detail) {
+                const unit = currentGroup.units[0]
+                mesh = await createPolymerTraceMesh(ctx, unit, mesh)
+                ValueCell.update(renderObject.values.drawCount, mesh.triangleCount * 3)
+                updateColor = true
+            }
+
+            if (!deepEqual(newProps.colorTheme, currentProps.colorTheme)) {
+                updateColor = true
+            }
+
+            if (updateColor) {
+                const elementCount = currentGroup.elements.length
+                if (ctx.shouldUpdate) await ctx.update('Computing trace colors');
+                createColors(currentGroup, elementCount, newProps.colorTheme, renderObject.values)
+            }
+
+            updateMeshValues(renderObject.values, newProps)
+            updateRenderableState(renderObject.state, newProps)
+
+            currentProps = newProps
+            return true
+        },
+        getLoci(pickingId: PickingId) {
+            const { objectId, instanceId, elementId } = pickingId
+            if (renderObject.id === objectId) {
+                const unit = currentGroup.units[instanceId]
+                const indices = OrderedSet.ofSingleton(elementId as StructureElement.UnitIndex);
+                return StructureElement.Loci([{ unit, indices }])
+            }
+            return EmptyLoci
+        },
+        mark(loci: Loci, action: MarkerAction) {
+            markElement(renderObject.values.tMarker, currentGroup, loci, action)
+        },
+        destroy() {
+            // TODO
+        }
+    }
+}

+ 69 - 0
src/mol-geo/representation/structure/visual/util/common.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { Unit } from 'mol-model/structure';
+import { Mat4 } from 'mol-math/linear-algebra'
+
+import { createUniformColor, ColorData } from '../../../../util/color-data';
+import { createUniformSize, SizeData } from '../../../../util/size-data';
+import { physicalSizeData } from '../../../../theme/structure/size/physical';
+import VertexMap from '../../../../shape/vertex-map';
+import { ColorTheme, SizeTheme } from '../../../../theme';
+import { elementIndexColorData, elementSymbolColorData, instanceIndexColorData, chainIdElementColorData } from '../../../../theme/structure/color';
+import { ValueCell, defaults } from 'mol-util';
+
+export function createTransforms({ units }: Unit.SymmetryGroup, transforms?: ValueCell<Float32Array>) {
+    const unitCount = units.length
+    const n = unitCount * 16
+    const array = transforms && transforms.ref.value.length >= n ? transforms.ref.value : new Float32Array(n)
+    for (let i = 0; i < unitCount; i++) {
+        Mat4.toArray(units[i].conformation.operator.matrix, array, i * 16)
+    }
+    return transforms ? ValueCell.update(transforms, array) : ValueCell.create(array)
+}
+
+const identityTransform = new Float32Array(16)
+Mat4.toArray(Mat4.identity(), identityTransform, 0)
+export function createIdentityTransform(transforms?: ValueCell<Float32Array>) {
+    return transforms ? ValueCell.update(transforms, identityTransform) : ValueCell.create(identityTransform)
+}
+
+export function createColors(group: Unit.SymmetryGroup, elementCount: number, props: ColorTheme, colorData?: ColorData) {
+    switch (props.name) {
+        case 'atom-index':
+            return elementIndexColorData({ group, elementCount }, colorData)
+        case 'chain-id':
+            return chainIdElementColorData({ group, elementCount }, colorData)
+        case 'element-symbol':
+            return elementSymbolColorData({ group, elementCount }, colorData)
+        case 'instance-index':
+            return instanceIndexColorData({ group, elementCount }, colorData)
+        case 'uniform':
+            return createUniformColor(props, colorData)
+    }
+}
+
+// export function createLinkColors(group: Unit.SymmetryGroup, props: ColorTheme, colorData?: ColorData): ColorData {
+//     switch (props.name) {
+//         case 'atom-index':
+//         case 'chain-id':
+//         case 'element-symbol':
+//         case 'instance-index':
+//             return chainIdLinkColorData({ group, vertexMap }, colorData)
+//         case 'uniform':
+//             return createUniformColor(props, colorData)
+//     }
+// }
+
+export function createSizes(group: Unit.SymmetryGroup, vertexMap: VertexMap, props: SizeTheme): SizeData {
+    switch (props.name) {
+        case 'uniform':
+            return createUniformSize(props)
+        case 'physical':
+            return physicalSizeData(defaults(props.factor, 1), { group, vertexMap })
+    }
+}

+ 103 - 0
src/mol-geo/representation/structure/visual/util/element.ts

@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { Unit, StructureElement } from 'mol-model/structure';
+import { SizeTheme } from '../../../../theme';
+import { RuntimeContext } from 'mol-task';
+import { sphereVertexCount } from '../../../../primitive/sphere';
+import { Mesh } from '../../../../shape/mesh';
+import { MeshBuilder } from '../../../../shape/mesh-builder';
+import { ValueCell, defaults } from 'mol-util';
+import { TextureImage } from 'mol-gl/renderable/util';
+import { Loci, isEveryLoci, EmptyLoci } from 'mol-model/loci';
+import { MarkerAction, applyMarkerAction } from '../../../../util/marker-data';
+import { Interval, OrderedSet } from 'mol-data/int';
+import { getPhysicalRadius } from '../../../../theme/structure/size/physical';
+import { PickingId } from '../../../../util/picking';
+
+export function getElementRadius(unit: Unit, props: SizeTheme): StructureElement.Property<number> {
+    switch (props.name) {
+        case 'uniform':
+            return () => props.value
+        case 'physical':
+            const radius = getPhysicalRadius(unit)
+            const factor = defaults(props.factor, 1)
+            return (l) => radius(l) * factor
+    }
+}
+
+export async function createElementSphereMesh(ctx: RuntimeContext, unit: Unit, radius: StructureElement.Property<number>, detail: number, mesh?: Mesh) {
+    const { elements } = unit;
+    const elementCount = elements.length;
+    const vertexCount = elementCount * sphereVertexCount(detail)
+    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
+
+    const v = Vec3.zero()
+    const pos = unit.conformation.invariantPosition
+    const l = StructureElement.create()
+    l.unit = unit
+
+    for (let i = 0; i < elementCount; i++) {
+        l.element = elements[i]
+        pos(elements[i], v)
+
+        meshBuilder.setId(i)
+        meshBuilder.addSphere(v, radius(l), detail)
+
+        if (i % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Sphere mesh', current: i, max: elementCount });
+        }
+    }
+
+    return meshBuilder.getMesh()
+}
+
+export function markElement(tMarker: ValueCell<TextureImage>, group: Unit.SymmetryGroup, loci: Loci, action: MarkerAction) {
+    let changed = false
+    const elementCount = group.elements.length
+    const instanceCount = group.units.length
+    const array = tMarker.ref.value.array
+    if (isEveryLoci(loci)) {
+        applyMarkerAction(array, 0, elementCount * instanceCount, action)
+        changed = true
+    } else if (StructureElement.isLoci(loci)) {
+        for (const e of loci.elements) {
+            const unitIdx = Unit.findUnitById(e.unit.id, group.units)
+            if (unitIdx !== -1) {
+                if (Interval.is(e.indices)) {
+                    const idxStart = unitIdx * elementCount + Interval.start(e.indices);
+                    const idxEnd = unitIdx * elementCount + Interval.end(e.indices);
+                    if (applyMarkerAction(array, idxStart, idxEnd, action) && !changed) {
+                        changed = true
+                    }
+                } else {
+                    for (let i = 0, _i = e.indices.length; i < _i; i++) {
+                        const idx = unitIdx * elementCount + e.indices[i];
+                        if (applyMarkerAction(array, idx, idx + 1, action) && !changed) {
+                            changed = true
+                        }
+                    }
+                }
+            }
+        }
+    } else {
+        return
+    }
+    if (changed) {
+        ValueCell.update(tMarker, tMarker.ref.value)
+    }
+}
+
+export function getElementLoci(id: number, group: Unit.SymmetryGroup, pickingId: PickingId) {
+    const { objectId, instanceId, elementId } = pickingId
+    if (id === objectId) {
+        const unit = group.units[instanceId]
+        const indices = OrderedSet.ofSingleton(elementId as StructureElement.UnitIndex);
+        return StructureElement.Loci([{ unit, indices }])
+    }
+    return EmptyLoci
+}

+ 122 - 0
src/mol-geo/representation/structure/visual/util/link.ts

@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Vec3 } from 'mol-math/linear-algebra';
+import { RuntimeContext } from 'mol-task';
+import { Mesh } from '../../../../shape/mesh';
+import { MeshBuilder } from '../../../../shape/mesh-builder';
+import { LinkType } from 'mol-model/structure/model/types';
+import { DefaultMeshProps } from '../../../util';
+
+export const DefaultLinkCylinderProps = {
+    ...DefaultMeshProps,
+    linkScale: 0.4,
+    linkSpacing: 1,
+    linkRadius: 0.25,
+    radialSegments: 16
+}
+export type LinkCylinderProps = typeof DefaultLinkCylinderProps
+
+const tmpShiftV12 = Vec3.zero()
+const tmpShiftV13 = Vec3.zero()
+
+/** Calculate 'shift' direction that is perpendiculat to v1 - v2 and goes through v3 */
+export function calculateShiftDir (out: Vec3, v1: Vec3, v2: Vec3, v3: Vec3 | null) {
+    Vec3.normalize(tmpShiftV12, Vec3.sub(tmpShiftV12, v1, v2))
+
+    if (v3 !== null) {
+        Vec3.sub(tmpShiftV13, v1, v3)
+    } else {
+        Vec3.copy(tmpShiftV13, v1)  // no reference point, use v1
+    }
+    Vec3.normalize(tmpShiftV13, tmpShiftV13)
+
+    // ensure v13 and v12 are not colinear
+    let dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+    if (1 - Math.abs(dp) < 1e-5) {
+        Vec3.set(tmpShiftV13, 1, 0, 0)
+        dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+        if (1 - Math.abs(dp) < 1e-5) {
+            Vec3.set(tmpShiftV13, 0, 1, 0)
+            dp = Vec3.dot(tmpShiftV12, tmpShiftV13)
+        }
+    }
+
+    Vec3.setMagnitude(tmpShiftV12, tmpShiftV12, dp)
+    Vec3.sub(tmpShiftV13, tmpShiftV13, tmpShiftV12)
+    return Vec3.normalize(out, tmpShiftV13)
+}
+
+export interface LinkCylinderMeshBuilderProps {
+    linkCount: number
+    referencePosition(edgeIndex: number): Vec3 | null
+    position(posA: Vec3, posB: Vec3, edgeIndex: number): void
+    order(edgeIndex: number): number
+    flags(edgeIndex: number): LinkType.Flag
+}
+
+/**
+ * Each edge is included twice to allow for coloring/picking
+ * the half closer to the first vertex, i.e. vertex a.
+ */
+export async function createLinkCylinderMesh(ctx: RuntimeContext, linkBuilder: LinkCylinderMeshBuilderProps, props: LinkCylinderProps, mesh?: Mesh) {
+    const { linkCount, referencePosition, position, order, flags } = linkBuilder
+
+    if (!linkCount) return Mesh.createEmpty(mesh)
+
+    // approximate vertextCount (* 2), exact calculation would need to take
+    // multiple cylinders for bond orders and metall coordinations into account
+    const vertexCount = props.radialSegments * 2 * linkCount * 2
+    const meshBuilder = MeshBuilder.create(vertexCount, vertexCount / 2, mesh)
+
+    const va = Vec3.zero()
+    const vb = Vec3.zero()
+    const vShift = Vec3.zero()
+
+    const { linkScale, linkSpacing, linkRadius, radialSegments } = props
+
+    const cylinderParams = {
+        height: 1,
+        radiusTop: linkRadius,
+        radiusBottom: linkRadius,
+        radialSegments
+    }
+
+    for (let edgeIndex = 0, _eI = linkCount; edgeIndex < _eI; ++edgeIndex) {
+        position(va, vb, edgeIndex)
+
+        const o = order(edgeIndex)
+        const f = flags(edgeIndex) as any as LinkType // TODO
+        meshBuilder.setId(edgeIndex)
+
+        if (LinkType.is(f, LinkType.Flag.MetallicCoordination)) {
+            // show metall coordinations with dashed cylinders
+            cylinderParams.radiusTop = cylinderParams.radiusBottom = linkRadius / 3
+            meshBuilder.addFixedCountDashedCylinder(va, vb, 0.5, 7, cylinderParams)
+        } else if (o === 2 || o === 3) {
+            // show bonds with order 2 or 3 using 2 or 3 parallel cylinders
+            const multiRadius = linkRadius * (linkScale / (0.5 * o))
+            const absOffset = (linkRadius - multiRadius) * linkSpacing
+
+            calculateShiftDir(vShift, va, vb, referencePosition(edgeIndex))
+            Vec3.setMagnitude(vShift, vShift, absOffset)
+
+            cylinderParams.radiusTop = cylinderParams.radiusBottom = multiRadius
+
+            if (o === 3) meshBuilder.addCylinder(va, vb, 0.5, cylinderParams)
+            meshBuilder.addDoubleCylinder(va, vb, 0.5, vShift, cylinderParams)
+        } else {
+            cylinderParams.radiusTop = cylinderParams.radiusBottom = linkRadius
+            meshBuilder.addCylinder(va, vb, 0.5, cylinderParams)
+        }
+
+        if (edgeIndex % 10000 === 0 && ctx.shouldUpdate) {
+            await ctx.update({ message: 'Cylinder mesh', current: edgeIndex, max: linkCount });
+        }
+    }
+
+    return meshBuilder.getMesh()
+}

+ 69 - 0
src/mol-geo/representation/structure/visual/util/polymer.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, ElementIndex } from 'mol-model/structure';
+import { Segmentation, OrderedSet, Interval } from 'mol-data/int';
+import SortedRanges from 'mol-data/int/sorted-ranges';
+
+export * from './polymer/backbone-iterator'
+export * from './polymer/gap-iterator'
+export * from './polymer/trace-iterator'
+export * from './polymer/curve-segment'
+
+export function getPolymerRanges(unit: Unit): SortedRanges<ElementIndex> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return unit.model.atomicHierarchy.polymerRanges
+        case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.polymerRanges
+        case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.polymerRanges
+    }
+}
+
+export function getGapRanges(unit: Unit): SortedRanges<ElementIndex> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return unit.model.atomicHierarchy.gapRanges
+        case Unit.Kind.Spheres: return unit.model.coarseHierarchy.spheres.gapRanges
+        case Unit.Kind.Gaussians: return unit.model.coarseHierarchy.gaussians.gapRanges
+    }
+}
+
+export function getPolymerElementCount(unit: Unit) {
+    let count = 0
+    const { elements } = unit
+    const polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), elements)
+    switch (unit.kind) {
+        case Unit.Kind.Atomic:
+            const residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, elements)
+            while (polymerIt.hasNext) {
+                const polymerSegment = polymerIt.move()
+                residueIt.setSegment(polymerSegment)
+                while (residueIt.hasNext) {
+                    const residueSegment = residueIt.move()
+                    const { start, end } = residueSegment
+                    if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+                }
+            }
+            break
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            while (polymerIt.hasNext) {
+                const { start, end } = polymerIt.move()
+                count += OrderedSet.intersectionSize(Interval.ofBounds(elements[start], elements[end - 1]), elements)
+            }
+            break
+    }
+    return count
+}
+
+export function getPolymerGapCount(unit: Unit) {
+    let count = 0
+    const { elements } = unit
+    const gapIt = SortedRanges.transientSegments(getGapRanges(unit), elements)
+    while (gapIt.hasNext) {
+        const { start, end } = gapIt.move()
+        if (OrderedSet.areIntersecting(Interval.ofBounds(elements[start], elements[end - 1]), elements)) ++count
+    }
+    return count
+}

+ 144 - 0
src/mol-geo/representation/structure/visual/util/polymer/backbone-iterator.ts

@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Unit, StructureElement, ElementIndex, ResidueIndex } from 'mol-model/structure';
+import { Segmentation, SortedArray } from 'mol-data/int';
+import Iterator from 'mol-data/iterator';
+import SortedRanges from 'mol-data/int/sorted-ranges';
+import { getElementIndexForAtomRole } from 'mol-model/structure/util';
+import { AtomRole } from 'mol-model/structure/model/types';
+import { getPolymerRanges } from '../polymer';
+
+/** Iterates over consecutive pairs of residues/coarse elements in polymers */
+export function PolymerBackboneIterator(unit: Unit): Iterator<PolymerBackbonePair> {
+    switch (unit.kind) {
+        case Unit.Kind.Atomic: return new AtomicPolymerBackboneIterator(unit)
+        case Unit.Kind.Spheres:
+        case Unit.Kind.Gaussians:
+            return new CoarsePolymerBackboneIterator(unit)
+    }
+}
+
+interface PolymerBackbonePair {
+    centerA: StructureElement
+    centerB: StructureElement
+}
+
+function createPolymerBackbonePair (unit: Unit) {
+    return {
+        centerA: StructureElement.create(unit),
+        centerB: StructureElement.create(unit),
+    }
+}
+
+const enum AtomicPolymerBackboneIteratorState { nextPolymer, firstResidue, nextResidue, cycle }
+
+export class AtomicPolymerBackboneIterator implements Iterator<PolymerBackbonePair> {
+    private value: PolymerBackbonePair
+    private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
+    private residueIt: Segmentation.SegmentIterator<ResidueIndex>
+    private state: AtomicPolymerBackboneIteratorState = AtomicPolymerBackboneIteratorState.nextPolymer
+    private residueSegment: Segmentation.Segment<ResidueIndex>
+    hasNext: boolean = false;
+
+    private getElementIndex(residueIndex: ResidueIndex, atomRole: AtomRole) {
+        const index = getElementIndexForAtomRole(this.unit.model, residueIndex, atomRole)
+        // TODO handle case when it returns -1
+        const elementIndex = SortedArray.indexOf(this.unit.elements, index) as ElementIndex
+        if (elementIndex === -1) {
+            console.log('-1', residueIndex, atomRole, index)
+        }
+        return elementIndex === -1 ? 0 as ElementIndex : elementIndex
+    }
+
+    move() {
+        if (this.state === AtomicPolymerBackboneIteratorState.nextPolymer) {
+            while (this.polymerIt.hasNext) {
+                this.residueIt.setSegment(this.polymerIt.move());
+                if (this.residueIt.hasNext) {
+                    this.residueSegment = this.residueIt.move()
+                    this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
+                    this.state = AtomicPolymerBackboneIteratorState.nextResidue
+                    break
+                }
+            }
+        }
+
+        if (this.state === AtomicPolymerBackboneIteratorState.nextResidue) {
+            this.residueSegment = this.residueIt.move()
+            this.value.centerA.element = this.value.centerB.element
+            this.value.centerB.element = this.getElementIndex(this.residueSegment.index, 'trace')
+            if (!this.residueIt.hasNext) {
+                if (this.unit.model.atomicHierarchy.cyclicPolymerMap.has(this.residueSegment.index)) {
+                    this.state = AtomicPolymerBackboneIteratorState.cycle
+                } else {
+                    // TODO need to advance to a polymer that has two or more residues (can't assume it has)
+                    this.state = AtomicPolymerBackboneIteratorState.nextPolymer
+                }
+            }
+        } else if (this.state === AtomicPolymerBackboneIteratorState.cycle) {
+            const { cyclicPolymerMap } = this.unit.model.atomicHierarchy
+            this.value.centerA.element = this.value.centerB.element
+            this.value.centerB.element = this.getElementIndex(cyclicPolymerMap.get(this.residueSegment.index)!, 'trace')
+            // TODO need to advance to a polymer that has two or more residues (can't assume it has)
+            this.state = AtomicPolymerBackboneIteratorState.nextPolymer
+        }
+
+        this.hasNext = this.residueIt.hasNext || this.polymerIt.hasNext || this.state === AtomicPolymerBackboneIteratorState.cycle
+        return this.value;
+    }
+
+    constructor(private unit: Unit.Atomic) {
+        this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements)
+        this.residueIt = Segmentation.transientSegments(unit.model.atomicHierarchy.residueAtomSegments, unit.elements)
+        this.value = createPolymerBackbonePair(unit)
+        this.hasNext = this.residueIt.hasNext && this.polymerIt.hasNext
+    }
+}
+
+const enum CoarsePolymerBackboneIteratorState { nextPolymer, nextElement }
+
+export class CoarsePolymerBackboneIterator implements Iterator<PolymerBackbonePair> {
+    private value: PolymerBackbonePair
+    private polymerIt: SortedRanges.Iterator<ElementIndex, ResidueIndex>
+    private polymerSegment: Segmentation.Segment<ResidueIndex>
+    private state: CoarsePolymerBackboneIteratorState = CoarsePolymerBackboneIteratorState.nextPolymer
+    private elementIndex: number
+    hasNext: boolean = false;
+
+    move() {
+        if (this.state === CoarsePolymerBackboneIteratorState.nextPolymer) {
+            if (this.polymerIt.hasNext) {
+                this.polymerSegment = this.polymerIt.move();
+                this.elementIndex = this.polymerSegment.start
+                if (this.elementIndex + 1 < this.polymerSegment.end) {
+                    this.value.centerB.element = this.unit.elements[this.elementIndex]
+                    this.state = CoarsePolymerBackboneIteratorState.nextElement
+                } else {
+                    this.state = CoarsePolymerBackboneIteratorState.nextPolymer
+                }
+            }
+        }
+
+        if (this.state === CoarsePolymerBackboneIteratorState.nextElement) {
+            this.elementIndex += 1
+            this.value.centerA.element = this.value.centerB.element
+            this.value.centerB.element = this.unit.elements[this.elementIndex]
+            if (this.elementIndex + 1 >= this.polymerSegment.end) {
+                this.state = CoarsePolymerBackboneIteratorState.nextPolymer
+            }
+        }
+
+        this.hasNext = this.elementIndex + 1 < this.polymerSegment.end || this.polymerIt.hasNext
+        return this.value;
+    }
+
+    constructor(private unit: Unit.Spheres | Unit.Gaussians) {
+        this.polymerIt = SortedRanges.transientSegments(getPolymerRanges(unit), unit.elements);
+        this.value = createPolymerBackbonePair(unit)
+        this.hasNext = this.polymerIt.hasNext
+    }
+}

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