Explorar el Código

Merge pull request #2 from molstar/ply-test

merge with current master and some cleanup
MarcoSchaeferT hace 6 años
padre
commit
8299977895
Se han modificado 100 ficheros con 5299 adiciones y 852 borrados
  1. 0 5
      .vscode/tasks.json
  2. 16 5
      README.md
  3. 5 6
      data/rcsb-graphql/codegen.js
  4. 14 0
      data/rcsb-graphql/loader.js
  5. 564 0
      docs/state/example-state.json
  6. 120 0
      docs/state/readme.md
  7. 738 0
      docs/state/transforms.md
  8. 412 376
      package-lock.json
  9. 24 20
      package.json
  10. 30 0
      src/apps/basic-wrapper/coloring.ts
  11. 128 0
      src/apps/basic-wrapper/index.html
  12. 139 0
      src/apps/basic-wrapper/index.ts
  13. 2 2
      src/apps/chem-comp-bond/create-table.ts
  14. 1 0
      src/apps/schema-generator/util/cif-dic.ts
  15. 69 0
      src/apps/state-docs/index.ts
  16. 75 0
      src/apps/state-docs/pd-to-md.ts
  17. 4 3
      src/apps/structure-info/model.ts
  18. 6 4
      src/apps/structure-info/volume.ts
  19. 171 0
      src/apps/viewer/extensions/jolecule.ts
  20. 8 1
      src/apps/viewer/index.html
  21. 46 3
      src/apps/viewer/index.ts
  22. 72 0
      src/examples/proteopedia-wrapper/annotation.ts
  23. 7 0
      src/examples/proteopedia-wrapper/changelog.md
  24. 100 0
      src/examples/proteopedia-wrapper/helpers.ts
  25. 155 0
      src/examples/proteopedia-wrapper/index.html
  26. 225 0
      src/examples/proteopedia-wrapper/index.ts
  27. 21 0
      src/examples/proteopedia-wrapper/ui/controls.tsx
  28. 44 20
      src/mol-canvas3d/canvas3d.ts
  29. 40 21
      src/mol-canvas3d/controls/trackball.ts
  30. 4 2
      src/mol-canvas3d/helper/bounding-sphere-helper.ts
  31. 125 0
      src/mol-canvas3d/helper/interaction-events.ts
  32. 3 0
      src/mol-data/int/_spec/ordered-set.spec.ts
  33. 1 1
      src/mol-data/int/impl/ordered-set.ts
  34. 1 1
      src/mol-data/int/impl/sorted-array.ts
  35. 4 2
      src/mol-geo/geometry/base.ts
  36. 4 4
      src/mol-geo/geometry/color-data.ts
  37. 6 3
      src/mol-geo/geometry/direct-volume/direct-volume.ts
  38. 30 12
      src/mol-geo/geometry/lines/lines-builder.ts
  39. 5 2
      src/mol-geo/geometry/lines/lines.ts
  40. 7 7
      src/mol-geo/geometry/marker-data.ts
  41. 15 6
      src/mol-geo/geometry/mesh/builder/sheet.ts
  42. 19 0
      src/mol-geo/geometry/mesh/builder/triangle.ts
  43. 10 1
      src/mol-geo/geometry/mesh/builder/tube.ts
  44. 61 1
      src/mol-geo/geometry/mesh/mesh-builder.ts
  45. 3 0
      src/mol-geo/geometry/mesh/mesh.ts
  46. 59 0
      src/mol-geo/geometry/overpaint-data.ts
  47. 3 0
      src/mol-geo/geometry/points/points.ts
  48. 3 3
      src/mol-geo/geometry/size-data.ts
  49. 1 1
      src/mol-geo/geometry/spheres/spheres-builder.ts
  50. 3 0
      src/mol-geo/geometry/spheres/spheres.ts
  51. 183 21
      src/mol-geo/geometry/text/text-builder.ts
  52. 25 8
      src/mol-geo/geometry/text/text.ts
  53. 47 8
      src/mol-geo/geometry/transform-data.ts
  54. 36 11
      src/mol-geo/primitive/box.ts
  55. 14 0
      src/mol-geo/primitive/cage.ts
  56. 69 0
      src/mol-geo/primitive/dodecahedron.ts
  57. 17 3
      src/mol-geo/primitive/icosahedron.ts
  58. 26 6
      src/mol-geo/primitive/octahedron.ts
  59. 10 0
      src/mol-geo/primitive/plane.ts
  60. 8 7
      src/mol-geo/primitive/polygon.ts
  61. 67 12
      src/mol-geo/primitive/prism.ts
  62. 52 16
      src/mol-geo/primitive/pyramid.ts
  63. 62 0
      src/mol-geo/primitive/spiked-ball.ts
  64. 36 0
      src/mol-geo/primitive/tetrahedron.ts
  65. 18 10
      src/mol-geo/primitive/wedge.ts
  66. 9 1
      src/mol-gl/_spec/renderer.spec.ts
  67. 6 1
      src/mol-gl/renderable.ts
  68. 11 1
      src/mol-gl/renderable/direct-volume.ts
  69. 52 4
      src/mol-gl/renderable/schema.ts
  70. 1 0
      src/mol-gl/renderable/text.ts
  71. 3 2
      src/mol-gl/renderable/util.ts
  72. 16 6
      src/mol-gl/scene.ts
  73. 4 0
      src/mol-gl/shader/chunks/assign-color-varying.glsl
  74. 5 0
      src/mol-gl/shader/chunks/assign-material-color.glsl
  75. 4 0
      src/mol-gl/shader/chunks/color-frag-params.glsl
  76. 6 0
      src/mol-gl/shader/chunks/color-vert-params.glsl
  77. 11 4
      src/mol-gl/shader/text.vert
  78. 33 21
      src/mol-gl/webgl/render-item.ts
  79. 2 4
      src/mol-gl/webgl/texture.ts
  80. 108 22
      src/mol-io/common/file-handle.ts
  81. 127 0
      src/mol-io/common/simple-buffer.ts
  82. 73 0
      src/mol-io/common/typed-array.ts
  83. 2 4
      src/mol-io/reader/_spec/ccp4.spec.ts
  84. 4 5
      src/mol-io/reader/_spec/cif.spec.ts
  85. 1 1
      src/mol-io/reader/_spec/csv.spec.ts
  86. 3 3
      src/mol-io/reader/_spec/gro.spec.ts
  87. 115 71
      src/mol-io/reader/ccp4/parser.ts
  88. 4 1
      src/mol-io/reader/ccp4/schema.ts
  89. 1 1
      src/mol-io/reader/cif/binary/parser.ts
  90. 159 2
      src/mol-io/reader/cif/data-model.ts
  91. 1 1
      src/mol-io/reader/cif/schema/bird.ts
  92. 1 1
      src/mol-io/reader/cif/schema/ccd.ts
  93. 3 3
      src/mol-io/reader/cif/schema/mmcif.ts
  94. 0 54
      src/mol-io/reader/cif/text/field.ts
  95. 4 5
      src/mol-io/reader/cif/text/parser.ts
  96. 55 15
      src/mol-io/reader/common/text/tokenizer.ts
  97. 2 2
      src/mol-io/reader/csv/field.ts
  98. 3 3
      src/mol-io/reader/csv/parser.ts
  99. 153 0
      src/mol-io/reader/dsn6/parser.ts
  100. 44 0
      src/mol-io/reader/dsn6/schema.ts

+ 0 - 5
.vscode/tasks.json

@@ -9,11 +9,6 @@
             "problemMatcher": [
                 "$tsc"
             ]
-        },
-        {
-            "type": "npm",
-            "script": "app-render-test",
-            "problemMatcher": []
         }
     ]
 }

+ 16 - 5
README.md

@@ -17,6 +17,7 @@ The core of Mol* currently consists of these modules:
 - `mol-math` Math related (loosely) algorithms and data structures.
 - `mol-io` Parsing library. Each format is parsed into an interface that corresponds to the data stored by it. Support for common coordinate, experimental/map, and annotation data formats.
 - `mol-model` Data structures and algorithms (such as querying) for representing molecular data (including coordinate, experimental/map, and annotation data).
+- `mol-model-formats` Data format parsers for `mol-model`.
 - `mol-model-props` Common "custom properties".
 - `mol-script` A scriting language for creating representations/scenes and querying (includes the [MolQL query language](https://molql.github.io)).
 - `mol-geo` Creating (molecular) geometries.
@@ -98,11 +99,11 @@ Run the image
 ### Code generation
 **CIF schemas**
 
-    node build/node_modules/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/mmcif.ts --fieldNamesPath data/mmcif-field-names.csv --name mmCIF
+    export NODE_PATH="build/src"; node build/src/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/mmcif.ts --fieldNamesPath data/mmcif-field-names.csv --name mmCIF
 
-    node build/node_modules/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/ccd.ts --fieldNamesPath data/ccd-field-names.csv --name CCD
+    export NODE_PATH="build/src"; node build/src/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/ccd.ts --fieldNamesPath data/ccd-field-names.csv --name CCD
 
-    node build/node_modules/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/bird.ts --fieldNamesPath data/bird-field-names.csv --name BIRD
+    export NODE_PATH="build/src"; node build/src/apps/schema-generator/schema-from-cif-dic.js -ts -o src/mol-io/reader/cif/schema/bird.ts --fieldNamesPath data/bird-field-names.csv --name BIRD
 
 **GraphQL schemas**
 
@@ -111,11 +112,21 @@ Run the image
 ### Other scripts
 **Create chem comp bond table**
 
-    node --max-old-space-size=8192 build/node_modules/apps/chem-comp-bond/create-table.js build/data/ccb.bcif -b
+    export NODE_PATH="build/src"; node --max-old-space-size=8192 build/src/apps/chem-comp-bond/create-table.js build/data/ccb.bcif -b
 
 **Test model server**
 
-    node build/node_modules/servers/model/test.js
+    export NODE_PATH="build/src"; node build/src/servers/model/test.js
+
+**State Transformer Docs**
+
+    export NODE_PATH="build/src"; node build/state-docs
+
+**Convert any CIF to BinaryCIF**
+
+    node build/model-server/preprocess -i file.cif -ob file.bcif
+
+To see all available commands, use ``node build/model-server/preprocess -h``.
 
 ## Contributing
 Just open an issue or make a pull request. All contributions are welcome.

+ 5 - 6
data/rcsb-graphql/codegen.js

@@ -5,17 +5,16 @@ const basePath = path.join(__dirname, '..', '..', 'src', 'mol-model-props', 'rcs
 
 generate({
     schema: 'http://rest-dev.rcsb.org/graphql',
-    documents: [
-        path.join(basePath, 'symmetry.gql.ts')
-    ],
+    documents: {
+        [path.join(basePath, 'symmetry.gql.ts')]: {
+            loader: path.join(__dirname, 'loader.js')
+        },
+    },
     generates: {
         [path.join(basePath, 'types.ts')]: {
             plugins: ['time', 'typescript-common', 'typescript-client']
         }
     },
-    // template: 'graphql-codegen-typescript-template',
-    // out: path.join(basePath),
-    // skipSchema: true,
     overwrite: true,
     config: path.join(__dirname, 'codegen.json')
 }, true).then(

+ 14 - 0
data/rcsb-graphql/loader.js

@@ -0,0 +1,14 @@
+const { parse } = require('graphql');
+const { readFileSync } = require('fs');
+
+module.exports = function(docString, config) {
+    const str = readFileSync(docString, { encoding: 'utf-8' }).trim()
+                    .replace(/^export default `/, '')
+                    .replace(/`$/, '')
+    return [
+        {
+            filePath: docString,
+            content: parse(str)
+        }
+    ];
+};

+ 564 - 0
docs/state/example-state.json

@@ -0,0 +1,564 @@
+{
+  "data": {
+    "tree": {
+      "transforms": [
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "build-in.root",
+            "params": {},
+            "props": {},
+            "ref": "-=root=-",
+            "version": "mzgKPzL3KrSARixuuQPCIQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.download",
+            "params": {
+              "url": "https://webchem.ncbr.muni.cz/ModelServer/static/bcif/1tqn",
+              "isBinary": true,
+              "label": "BinaryCIF: 1tqn"
+            },
+            "props": {},
+            "ref": "OV8KkYn5g27qN191asD6CA",
+            "version": "1FyKSTffbKL7OJumHR7wEA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "OV8KkYn5g27qN191asD6CA",
+            "transformer": "ms-plugin.parse-cif",
+            "props": {},
+            "ref": "SXZ2y1ywkdn-rF4J6yVtKw",
+            "version": "459vIHyqOSKrvNFJUyCgNA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "SXZ2y1ywkdn-rF4J6yVtKw",
+            "transformer": "ms-plugin.trajectory-from-mmcif",
+            "props": {},
+            "ref": "gOuSu4Fnrokcj2q6K15cBw",
+            "version": "lBYS-wawjgY_HkvRr_N3_g"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "gOuSu4Fnrokcj2q6K15cBw",
+            "transformer": "ms-plugin.model-from-trajectory",
+            "params": {
+              "modelIndex": 0
+            },
+            "props": {},
+            "ref": "smMTjktic5g0ZHWpp5ONQg",
+            "version": "cPXInj4lXxoOvarcihICOw"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "smMTjktic5g0ZHWpp5ONQg",
+            "transformer": "ms-plugin.structure-assembly-from-model",
+            "props": {},
+            "ref": "Md3saiWEqsJYXifvMiW3Pg",
+            "version": "jFOatV4JJryP2RGqmhKuCQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "Md3saiWEqsJYXifvMiW3Pg",
+            "transformer": "ms-plugin.structure-complex-element",
+            "params": {
+              "type": "atomic-sequence"
+            },
+            "props": {},
+            "ref": "wfofDbgdMllp3ACC4D8geQ",
+            "version": "eL018xqw_Qa2p0Lbn5S0pA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "wfofDbgdMllp3ACC4D8geQ",
+            "transformer": "ms-plugin.structure-representation-3d",
+            "params": {
+              "type": {
+                "name": "cartoon",
+                "params": {
+                  "alpha": 1,
+                  "useFog": true,
+                  "highlightColor": 16737945,
+                  "selectColor": 3407641,
+                  "quality": "auto",
+                  "doubleSided": false,
+                  "flipSided": false,
+                  "flatShaded": false,
+                  "unitKinds": [
+                    "atomic",
+                    "spheres"
+                  ],
+                  "sizeFactor": 0.2,
+                  "linearSegments": 8,
+                  "radialSegments": 16,
+                  "aspectRatio": 5,
+                  "arrowFactor": 1.5,
+                  "visuals": [
+                    "polymer-trace",
+                    "polymer-gap",
+                    "nucleotide-block"
+                  ]
+                }
+              },
+              "colorTheme": {
+                "name": "polymer-id",
+                "params": {
+                  "list": "RedYellowBlue"
+                }
+              },
+              "sizeTheme": {
+                "name": "uniform",
+                "params": {
+                  "value": 1
+                }
+              }
+            },
+            "props": {},
+            "ref": "1nhs1yOSXXGKYl9m0fo6OQ",
+            "version": "2fnNcBIrZE-1Nz-MK-OmIQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "Md3saiWEqsJYXifvMiW3Pg",
+            "transformer": "ms-plugin.structure-complex-element",
+            "params": {
+              "type": "atomic-het"
+            },
+            "props": {},
+            "ref": "u50nXO1GHQAropjVQ7krqQ",
+            "version": "rlsAA6L34NY5zDZqpzYOiA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "u50nXO1GHQAropjVQ7krqQ",
+            "transformer": "ms-plugin.structure-representation-3d",
+            "params": {
+              "type": {
+                "name": "ball-and-stick",
+                "params": {
+                  "alpha": 1,
+                  "useFog": true,
+                  "highlightColor": 16737945,
+                  "selectColor": 3407641,
+                  "quality": "auto",
+                  "doubleSided": false,
+                  "flipSided": false,
+                  "flatShaded": false,
+                  "unitKinds": [
+                    "atomic"
+                  ],
+                  "sizeFactor": 0.3,
+                  "detail": 0,
+                  "linkScale": 0.4,
+                  "linkSpacing": 1,
+                  "radialSegments": 16,
+                  "sizeAspectRatio": 0.6666666666666666,
+                  "visuals": [
+                    "element-sphere",
+                    "intra-link",
+                    "inter-link"
+                  ]
+                }
+              },
+              "colorTheme": {
+                "name": "element-symbol",
+                "params": {}
+              },
+              "sizeTheme": {
+                "name": "uniform",
+                "params": {
+                  "value": 1
+                }
+              }
+            },
+            "props": {},
+            "ref": "PpDdNcH48Zgc8ezoAUodCQ",
+            "version": "EWyZJzX4S-F04f3YHXviXA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "Md3saiWEqsJYXifvMiW3Pg",
+            "transformer": "ms-plugin.structure-complex-element",
+            "params": {
+              "type": "water"
+            },
+            "props": {},
+            "ref": "ewximnxuhkX3AUj1oRvEOQ",
+            "version": "ejhvuA9YmwGzk-sa2lqpZQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "ewximnxuhkX3AUj1oRvEOQ",
+            "transformer": "ms-plugin.structure-representation-3d",
+            "params": {
+              "type": {
+                "name": "ball-and-stick",
+                "params": {
+                  "alpha": 0.51,
+                  "useFog": true,
+                  "highlightColor": 16737945,
+                  "selectColor": 3407641,
+                  "quality": "auto",
+                  "doubleSided": false,
+                  "flipSided": false,
+                  "flatShaded": false,
+                  "unitKinds": [
+                    "atomic"
+                  ],
+                  "sizeFactor": 0.3,
+                  "detail": 0,
+                  "linkScale": 0.4,
+                  "linkSpacing": 1,
+                  "radialSegments": 16,
+                  "sizeAspectRatio": 0.6666666666666666,
+                  "visuals": [
+                    "element-sphere",
+                    "intra-link",
+                    "inter-link"
+                  ]
+                }
+              },
+              "colorTheme": {
+                "name": "element-symbol",
+                "params": {}
+              },
+              "sizeTheme": {
+                "name": "uniform",
+                "params": {
+                  "value": 1
+                }
+              }
+            },
+            "props": {},
+            "ref": "Hxy9RPnjdttpe012E76EKA",
+            "version": "Zdyr_ux94ld0ZwlS9PC0Hg"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "Md3saiWEqsJYXifvMiW3Pg",
+            "transformer": "ms-plugin.structure-complex-element",
+            "params": {
+              "type": "spheres"
+            },
+            "props": {},
+            "ref": "vapkcyYj-YZRiU6yXMRDqg",
+            "version": "uwxGVj7daumXxR4emr_Wtg"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "vapkcyYj-YZRiU6yXMRDqg",
+            "transformer": "ms-plugin.structure-representation-3d",
+            "params": {
+              "type": {
+                "name": "spacefill",
+                "params": {
+                  "alpha": 1,
+                  "useFog": true,
+                  "highlightColor": 16737945,
+                  "selectColor": 3407641,
+                  "quality": "auto",
+                  "doubleSided": false,
+                  "flipSided": false,
+                  "flatShaded": false,
+                  "unitKinds": [
+                    "atomic",
+                    "spheres"
+                  ],
+                  "sizeFactor": 1,
+                  "detail": 0
+                }
+              },
+              "colorTheme": {
+                "name": "element-symbol",
+                "params": {}
+              },
+              "sizeTheme": {
+                "name": "physical",
+                "params": {}
+              }
+            },
+            "props": {},
+            "ref": "QpCCFEvPS0cF0yTeDhO9Zg",
+            "version": "n6UGlNqXLX3vgaU8hYfyGQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ]
+      ]
+    }
+  },
+  "behaviour": {
+    "tree": {
+      "transforms": [
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "build-in.root",
+            "params": {},
+            "props": {},
+            "ref": "-=root=-",
+            "version": "_nQeC9QaAh9q6OITzdzrIA"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.representation-highlight-loci",
+            "props": {},
+            "ref": "ms-plugin.representation-highlight-loci",
+            "version": "ATUn8B_HnbqTgl24tHA8og"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.representation-select-loci",
+            "props": {},
+            "ref": "ms-plugin.representation-select-loci",
+            "version": "4zLbjE8cn7XZGvS1b6ICKQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.default-loci-label-provider",
+            "props": {},
+            "ref": "ms-plugin.default-loci-label-provider",
+            "version": "CAynbi7XFxc8YVo8uZuGnQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.focus-loci-on-select",
+            "params": {
+              "minRadius": 20,
+              "extraRadius": 4
+            },
+            "props": {},
+            "ref": "ms-plugin.focus-loci-on-select",
+            "version": "lDDOcNcU6pTvO_U1xwR7-w"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.structure-animation",
+            "params": {
+              "rotate": false,
+              "rotateValue": 0,
+              "explode": false,
+              "explodeValue": 0
+            },
+            "props": {},
+            "ref": "ms-plugin.structure-animation",
+            "version": "xVHPb06oYJg14-PAb8c7Ng"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.scene-labels",
+            "props": {},
+            "ref": "ms-plugin.scene-labels",
+            "version": "lWoU9ybKTGzbJBhBR5McFg"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.pdbe-structure-quality-report-prop",
+            "params": {
+              "autoAttach": true
+            },
+            "props": {},
+            "ref": "ms-plugin.pdbe-structure-quality-report-prop",
+            "version": "oNuidegmrNmDom4UXjLAqg"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ],
+        [
+          {
+            "parent": "-=root=-",
+            "transformer": "ms-plugin.rcsb-assembly-symmetry-prop",
+            "params": {
+              "autoAttach": true
+            },
+            "props": {},
+            "ref": "ms-plugin.rcsb-assembly-symmetry-prop",
+            "version": "ca1Ihym2CC0KgwRtZWQKbQ"
+          },
+          {
+            "isHidden": false,
+            "isCollapsed": false
+          }
+        ]
+      ]
+    }
+  },
+  "cameraSnapshots": {
+    "entries": []
+  },
+  "canvas3d": {
+    "camera": {
+      "mode": "perspective",
+      "position": [
+        0,
+        0,
+        93.5120150707393
+      ],
+      "direction": [
+        -0.17921613100638745,
+        -0.1768933191370578,
+        -0.9677759720264687
+      ],
+      "up": [
+        -0.0322100883560256,
+        0.9842300308589832,
+        -0.1739360703345399
+      ],
+      "target": [
+        -20.84341143463866,
+        -20.57326095652141,
+        -19.043437656817368
+      ],
+      "near": 72.49875551585009,
+      "far": 161.0016342373897,
+      "fogNear": 116.30321064065254,
+      "fogFar": 161.0016342373897,
+      "fov": 0.7853981633974483,
+      "zoom": 8.988182413445411
+    },
+    "viewport": {
+      "cameraMode": "perspective",
+      "backgroundColor": 16579577,
+      "cameraClipDistance": 0,
+      "clip": [
+        1,
+        100
+      ],
+      "fog": [
+        50,
+        100
+      ],
+      "pickingAlphaThreshold": 0.5,
+      "trackball": {
+        "noScroll": true,
+        "rotateSpeed": 5,
+        "zoomSpeed": 6,
+        "panSpeed": 0.8,
+        "spin": false,
+        "spinSpeed": 1,
+        "staticMoving": true,
+        "dynamicDampingFactor": 0.2,
+        "minDistance": 0.01,
+        "maxDistance": 1e+150
+      },
+      "debug": {
+        "sceneBoundingSpheres": false,
+        "objectBoundingSpheres": false,
+        "instanceBoundingSpheres": false
+      }
+    }
+  }
+}

+ 120 - 0
docs/state/readme.md

@@ -0,0 +1,120 @@
+# Plugin State Representation
+
+The state of the plugin is represented by a JS Object with these components (described in more detail below):
+
+```ts
+interface Snapshot {
+    // Snapshot of data state tree
+    data?: State.Snapshot,
+    // Snapshot of behavior state tree
+    behaviour?: State.Snapshot,
+    // Snapshot for current animation,
+    animation?: PluginAnimationManager.Snapshot,
+    // Saved camera positions
+    cameraSnapshots?: CameraSnapshotManager.StateSnapshot,
+    canvas3d?: {
+        // Current camera position
+        camera?: Camera.Snapshot,
+        // Viewport properties such as background color
+        viewport?: Canvas3DProps
+    }
+}
+```
+
+When defining the state object, all components are optional, i.e., it is possible to define just the ``data`` component.
+
+Example state is available [here](example-state.json). In the plugin, it is possible to create and load these objects using ``Download JSON`` 
+and ``Open JSON`` buttons in the ``State Snapshots`` section.
+
+# State Tree
+
+The data and behavior of the plugin is stored in a tree data structure implemented in the ``mol-state`` module. This data structure 
+strictly separates the definition of the state with its actual instantiation, similar to the relation of HTML and DOM in web browsers.
+
+The snapshot itself is a JS Object with these components
+
+```ts
+interface State.Snapshot {
+    tree: StateTree.Serialized
+}
+
+interface StateTree.Serialized {
+    // Transforms serialized in pre-order
+    // The first transform must always be a special "root" node with ref: '-=root=-'
+    transforms: [Transform.Serialized, StateObjectCell.State][]
+}
+
+interface Transform.Serialized {
+    // id of the parent transform
+    parent: string,
+    // id of the corresponding transformer
+    transformer: string,
+    // parameters of the transform
+    params: any,
+    // Properties
+    props: Transform.Props,
+    // reference to this transform node (a unique string, can be UUID)
+    ref: string,
+    // version of the node (a unique string, can be UUID)
+    version: string
+}
+
+interface Transform.Props {
+    // tag used in state related operation
+    tag?: string
+    // is the node visible in the UI
+    isGhost?: boolean,
+    // is the node bound to its parent? (shown as a single node in the UI)
+    isBinding?: boolean
+}
+```
+
+"Built-in" data state transforms and description of their parameters are defined in ``mol-plugin/state/transforms``. Behavior transforms are defined in ``mol-plugin/behavior``. Auto-generated documentation for the transforms is also [available](transforms.md).
+
+# Animation State
+
+Defined by ``CameraSnapshotManager.StateSnapshot`` in ``mol-plugin/state/animation/manager.ts``.
+
+# Canvas3D State
+
+Defined by ``Canvas3DParams`` in ``mol-canvas3d/canvas3d.ts``.
+
+# Camera Snapshots
+
+The camera position (defined in ``mol-canvas3d/camera.ts``) is a plain JS object with the type:
+
+```ts
+interface Camera.Snapshot {
+    mode: Mode, // = 'perspective' | 'orthographic'
+
+    position: Vec3, // array with [x, y, z]
+    // Normalized camera direction
+    direction: Vec3, // array with [x, y, z]
+    up: Vec3, // array with [x, y, z]
+    target: Vec3, // array with [x, y, z]
+
+    near: number,
+    far: number,
+    fogNear: number,
+    fogFar: number,
+
+    fov: number,
+    zoom: number
+}
+```
+
+The ``cameraSnapshots`` component of the state are defined in ``mol-plugin/state/camera.ts``
+
+```js
+interface CameraSnapshotManager.StateSnapshot {
+    entries: Entry[]
+}
+
+interface Entry {
+    id: UUID, // or any string
+    timestamp: string, // timestamp usually in UTC format
+    name?: string, // optional name
+    description?: string, // optional description
+    snapshot: Camera.Snapshot
+}
+```

+ 738 - 0
docs/state/transforms.md

@@ -0,0 +1,738 @@
+# Mol* Plugin State Transformer Reference
+
+* [build-in.root](#build-in-root)
+* [ms-plugin.download](#ms-plugin-download)
+* [ms-plugin.read-file](#ms-plugin-read-file)
+* [ms-plugin.parse-cif](#ms-plugin-parse-cif)
+* [ms-plugin.parse-ccp4](#ms-plugin-parse-ccp4)
+* [ms-plugin.parse-dsn6](#ms-plugin-parse-dsn6)
+* [ms-plugin.trajectory-from-mmcif](#ms-plugin-trajectory-from-mmcif)
+* [ms-plugin.trajectory-from-pdb](#ms-plugin-trajectory-from-pdb)
+* [ms-plugin.model-from-trajectory](#ms-plugin-model-from-trajectory)
+* [ms-plugin.structure-from-model](#ms-plugin-structure-from-model)
+* [ms-plugin.structure-assembly-from-model](#ms-plugin-structure-assembly-from-model)
+* [ms-plugin.structure-symmetry-from-model](#ms-plugin-structure-symmetry-from-model)
+* [ms-plugin.structure-selection](#ms-plugin-structure-selection)
+* [ms-plugin.structure-complex-element](#ms-plugin-structure-complex-element)
+* [ms-plugin.custom-model-properties](#ms-plugin-custom-model-properties)
+* [ms-plugin.volume-from-ccp4](#ms-plugin-volume-from-ccp4)
+* [ms-plugin.volume-from-dsn6](#ms-plugin-volume-from-dsn6)
+* [ms-plugin.representation-highlight-loci](#ms-plugin-representation-highlight-loci)
+* [ms-plugin.representation-select-loci](#ms-plugin-representation-select-loci)
+* [ms-plugin.default-loci-label-provider](#ms-plugin-default-loci-label-provider)
+* [ms-plugin.structure-representation-3d](#ms-plugin-structure-representation-3d)
+* [ms-plugin.explode-structure-representation-3d](#ms-plugin-explode-structure-representation-3d)
+* [ms-plugin.volume-representation-3d](#ms-plugin-volume-representation-3d)
+* [ms-plugin.focus-loci-on-select](#ms-plugin-focus-loci-on-select)
+* [ms-plugin.pdbe-structure-quality-report-prop](#ms-plugin-pdbe-structure-quality-report-prop)
+* [ms-plugin.rcsb-assembly-symmetry-prop](#ms-plugin-rcsb-assembly-symmetry-prop)
+* [ms-plugin.structure-animation](#ms-plugin-structure-animation)
+* [ms-plugin.scene-labels](#ms-plugin-scene-labels)
+
+----------------------------
+## <a name="build-in-root"></a>build-in.root :: () -> ()
+*For internal use.*
+
+----------------------------
+## <a name="ms-plugin-download"></a>ms-plugin.download :: Root -> String | Binary
+*Download string or binary data from the specified URL*
+
+### Parameters
+- **url**: String *(Resource URL. Must be the same domain or support CORS.)*
+- **label**?: String
+- **isBinary**?: true/false *(If true, download data as binary (string otherwise))*
+
+### Default Parameters
+```js
+{
+  "url": "https://www.ebi.ac.uk/pdbe/static/entry/1cbs_updated.cif"
+}
+```
+----------------------------
+## <a name="ms-plugin-read-file"></a>ms-plugin.read-file :: Root -> String | Binary
+*Read string or binary data from the specified file*
+
+### Parameters
+- **file**: JavaScript File Handle
+- **label**?: String
+- **isBinary**?: true/false *(If true, open file as as binary (string otherwise))*
+
+### Default Parameters
+```js
+{}
+```
+----------------------------
+## <a name="ms-plugin-parse-cif"></a>ms-plugin.parse-cif :: String | Binary -> Cif
+*Parse CIF from String or Binary data*
+
+----------------------------
+## <a name="ms-plugin-parse-ccp4"></a>ms-plugin.parse-ccp4 :: Binary -> Ccp4
+*Parse CCP4/MRC/MAP from Binary data*
+
+----------------------------
+## <a name="ms-plugin-parse-dsn6"></a>ms-plugin.parse-dsn6 :: Binary -> Dsn6
+*Parse CCP4/BRIX from Binary data*
+
+----------------------------
+## <a name="ms-plugin-trajectory-from-mmcif"></a>ms-plugin.trajectory-from-mmcif :: Cif -> Trajectory
+*Identify and create all separate models in the specified CIF data block*
+
+### Parameters
+- **blockHeader**?: String *(Header of the block to parse. If none is specifed, the 1st data block in the file is used.)*
+
+### Default Parameters
+```js
+{}
+```
+----------------------------
+## <a name="ms-plugin-trajectory-from-pdb"></a>ms-plugin.trajectory-from-pdb :: String -> Trajectory
+
+----------------------------
+## <a name="ms-plugin-model-from-trajectory"></a>ms-plugin.model-from-trajectory :: Trajectory -> Model
+*Create a molecular structure from the specified model.*
+
+### Parameters
+- **modelIndex**: Numeric value *(Zero-based index of the model)*
+
+### Default Parameters
+```js
+{
+  "modelIndex": 0
+}
+```
+----------------------------
+## <a name="ms-plugin-structure-from-model"></a>ms-plugin.structure-from-model :: Model -> Structure
+*Create a molecular structure from the specified model.*
+
+----------------------------
+## <a name="ms-plugin-structure-assembly-from-model"></a>ms-plugin.structure-assembly-from-model :: Model -> Structure
+*Create a molecular structure assembly.*
+
+### Parameters
+- **id**?: String *(Assembly Id. Value 'deposited' can be used to specify deposited asymmetric unit.)*
+
+### Default Parameters
+```js
+{}
+```
+----------------------------
+## <a name="ms-plugin-structure-symmetry-from-model"></a>ms-plugin.structure-symmetry-from-model :: Model -> Structure
+*Create a molecular structure symmetry.*
+
+### Parameters
+- **ijkMin**: 3D vector [x, y, z]
+- **ijkMax**: 3D vector [x, y, z]
+
+### Default Parameters
+```js
+{
+  "ijkMin": [
+    -1,
+    -1,
+    -1
+  ],
+  "ijkMax": [
+    1,
+    1,
+    1
+  ]
+}
+```
+----------------------------
+## <a name="ms-plugin-structure-selection"></a>ms-plugin.structure-selection :: Structure -> Structure
+*Create a molecular structure from the specified query expression.*
+
+### Parameters
+- **query**: Value
+- **label**?: String
+
+### Default Parameters
+```js
+{}
+```
+----------------------------
+## <a name="ms-plugin-structure-complex-element"></a>ms-plugin.structure-complex-element :: Structure -> Structure
+*Create a molecular structure from the specified model.*
+
+### Parameters
+- **type**: One of 'atomic-sequence', 'water', 'atomic-het', 'spheres'
+
+### Default Parameters
+```js
+{
+  "type": "atomic-sequence"
+}
+```
+----------------------------
+## <a name="ms-plugin-custom-model-properties"></a>ms-plugin.custom-model-properties :: Model -> Model
+
+### Parameters
+- **properties**: Array of  *(A list of property descriptor ids.)*
+
+### Default Parameters
+```js
+{
+  "properties": []
+}
+```
+----------------------------
+## <a name="ms-plugin-volume-from-ccp4"></a>ms-plugin.volume-from-ccp4 :: Ccp4 -> Data
+*Create Volume from CCP4/MRC/MAP data*
+
+### Parameters
+- **voxelSize**: 3D vector [x, y, z]
+
+### Default Parameters
+```js
+{
+  "voxelSize": [
+    1,
+    1,
+    1
+  ]
+}
+```
+----------------------------
+## <a name="ms-plugin-volume-from-dsn6"></a>ms-plugin.volume-from-dsn6 :: Dsn6 -> Data
+*Create Volume from DSN6/BRIX data*
+
+### Parameters
+- **voxelSize**: 3D vector [x, y, z]
+
+### Default Parameters
+```js
+{
+  "voxelSize": [
+    1,
+    1,
+    1
+  ]
+}
+```
+----------------------------
+## <a name="ms-plugin-representation-highlight-loci"></a>ms-plugin.representation-highlight-loci :: Root -> Behavior
+
+----------------------------
+## <a name="ms-plugin-representation-select-loci"></a>ms-plugin.representation-select-loci :: Root -> Behavior
+
+----------------------------
+## <a name="ms-plugin-default-loci-label-provider"></a>ms-plugin.default-loci-label-provider :: Root -> Behavior
+
+----------------------------
+## <a name="ms-plugin-structure-representation-3d"></a>ms-plugin.structure-representation-3d :: Structure -> Representation3D
+
+### Parameters
+- **type**: Object { name: string, params: object } where name+params are:
+  - **cartoon**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **sizeFactor**: Numeric value
+      - **linearSegments**: Numeric value
+      - **radialSegments**: Numeric value
+      - **aspectRatio**: Numeric value
+      - **arrowFactor**: Numeric value
+      - **visuals**: Array of 'polymer-trace', 'polymer-gap', 'nucleotide-block', 'direction-wedge'
+
+  - **ball-and-stick**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **sizeFactor**: Numeric value
+      - **detail**: Numeric value
+      - **linkScale**: Numeric value
+      - **linkSpacing**: Numeric value
+      - **radialSegments**: Numeric value
+      - **sizeAspectRatio**: Numeric value
+      - **visuals**: Array of 'element-sphere', 'intra-link', 'inter-link'
+
+  - **carbohydrate**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **detail**: Numeric value
+      - **sizeFactor**: Numeric value
+      - **linkScale**: Numeric value
+      - **linkSpacing**: Numeric value
+      - **radialSegments**: Numeric value
+      - **linkSizeFactor**: Numeric value
+      - **visuals**: Array of 'carbohydrate-symbol', 'carbohydrate-link', 'carbohydrate-terminal-link'
+
+  - **distance-restraint**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **linkScale**: Numeric value
+      - **linkSpacing**: Numeric value
+      - **radialSegments**: Numeric value
+      - **sizeFactor**: Numeric value
+
+  - **molecular-surface**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **resolution**: Numeric value
+      - **radiusOffset**: Numeric value
+      - **smoothness**: Numeric value
+      - **useGpu**: true/false
+      - **ignoreCache**: true/false
+      - **sizeFactor**: Numeric value
+      - **lineSizeAttenuation**: true/false
+      - **visuals**: Array of 'gaussian-surface', 'gaussian-wireframe'
+
+  - **molecular-volume**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)*
+      - **renderMode**: One of 'isosurface', 'volume'
+      - **controlPoints**: A list of 2d vectors [xi, yi][]
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **resolution**: Numeric value
+      - **radiusOffset**: Numeric value
+      - **smoothness**: Numeric value
+
+  - **point**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **sizeFactor**: Numeric value
+      - **pointSizeAttenuation**: true/false
+      - **pointFilledCircle**: true/false
+      - **pointEdgeBleach**: Numeric value
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+
+  - **spacefill**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **unitKinds**: Array of 'atomic', 'spheres', 'gaussians'
+      - **sizeFactor**: Numeric value
+      - **detail**: Numeric value
+
+
+- **colorTheme**: Object { name: string, params: object } where name+params are:
+  - **carbohydrate-symbol**:
+Object with:
+
+  - **chain-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **cross-link**:
+Object with:
+      - **domain**: Interval [min, max]
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **element-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **element-symbol**:
+Object with:
+
+  - **molecule-type**:
+Object with:
+
+  - **polymer-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **polymer-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **residue-name**:
+Object with:
+
+  - **secondary-structure**:
+Object with:
+
+  - **sequence-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **shape-group**:
+Object with:
+
+  - **unit-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **uniform**:
+Object with:
+      - **value**: Color as 0xrrggbb
+
+
+- **sizeTheme**: Object { name: string, params: object } where name+params are:
+  - **physical**:
+Object with:
+
+  - **shape-group**:
+Object with:
+
+  - **uniform**:
+Object with:
+      - **value**: Numeric value
+
+
+
+### Default Parameters
+```js
+{
+  "type": {
+    "name": "cartoon",
+    "params": {
+      "alpha": 1,
+      "useFog": true,
+      "highlightColor": 16737945,
+      "selectColor": 3407641,
+      "quality": "auto",
+      "doubleSided": false,
+      "flipSided": false,
+      "flatShaded": false,
+      "unitKinds": [
+        "atomic",
+        "spheres"
+      ],
+      "sizeFactor": 0.2,
+      "linearSegments": 8,
+      "radialSegments": 16,
+      "aspectRatio": 5,
+      "arrowFactor": 1.5,
+      "visuals": [
+        "polymer-trace"
+      ]
+    }
+  },
+  "colorTheme": {
+    "name": "polymer-id",
+    "params": {
+      "list": "RedYellowBlue"
+    }
+  },
+  "sizeTheme": {
+    "name": "uniform",
+    "params": {
+      "value": 1
+    }
+  }
+}
+```
+----------------------------
+## <a name="ms-plugin-explode-structure-representation-3d"></a>ms-plugin.explode-structure-representation-3d :: Representation3D -> Obj
+
+### Parameters
+- **t**: Numeric value
+
+### Default Parameters
+```js
+{
+  "t": 0
+}
+```
+----------------------------
+## <a name="ms-plugin-volume-representation-3d"></a>ms-plugin.volume-representation-3d :: Data -> Representation3D
+
+### Parameters
+- **type**: Object { name: string, params: object } where name+params are:
+  - **isosurface**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **doubleSided**: true/false
+      - **flipSided**: true/false
+      - **flatShaded**: true/false
+      - **isoValue**:       - **absolute**: Numeric value
+      - **relative**: Numeric value
+
+      - **sizeFactor**: Numeric value
+      - **lineSizeAttenuation**: true/false
+      - **visuals**: Array of 'solid', 'wireframe'
+
+  - **direct-volume**:
+Object with:
+      - **alpha**: Numeric value
+      - **useFog**: true/false
+      - **highlightColor**: Color as 0xrrggbb
+      - **selectColor**: Color as 0xrrggbb
+      - **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+      - **isoValueNorm**: Numeric value *(Normalized Isolevel Value)*
+      - **renderMode**: One of 'isosurface', 'volume'
+      - **controlPoints**: A list of 2d vectors [xi, yi][]
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+
+- **colorTheme**: Object { name: string, params: object } where name+params are:
+  - **carbohydrate-symbol**:
+Object with:
+
+  - **chain-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **cross-link**:
+Object with:
+      - **domain**: Interval [min, max]
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **element-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **element-symbol**:
+Object with:
+
+  - **molecule-type**:
+Object with:
+
+  - **polymer-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **polymer-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **residue-name**:
+Object with:
+
+  - **secondary-structure**:
+Object with:
+
+  - **sequence-id**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **shape-group**:
+Object with:
+
+  - **unit-index**:
+Object with:
+      - **list**: One of 'OrangeRed', 'PurpleBlue', 'BluePurple', 'Oranges', 'BlueGreen', 'YellowOrangeBrown', 'YellowGreen', 'Reds', 'RedPurple', 'Greens', 'YellowGreenBlue', 'Purples', 'GreenBlue', 'Greys', 'YellowOrangeRed', 'PurpleRed', 'Blues', 'PurpleBlueGreen', 'Spectral', 'RedYellowGreen', 'RedBlue', 'PinkYellowGreen', 'PurpleGreen', 'RedYellowBlue', 'BrownWhiteGreen', 'RedGrey', 'PurpleOrange', 'Set2', 'Accent', 'Set1', 'Set3', 'Dark2', 'Paired', 'Pastel2', 'Pastel1', 'Magma', 'Inferno', 'Plasma', 'Viridis', 'Cividis', 'Twilight', 'Rainbow', 'RedWhiteBlue'
+
+  - **uniform**:
+Object with:
+      - **value**: Color as 0xrrggbb
+
+
+- **sizeTheme**: Object { name: string, params: object } where name+params are:
+  - **physical**:
+Object with:
+
+  - **shape-group**:
+Object with:
+
+  - **uniform**:
+Object with:
+      - **value**: Numeric value
+
+
+
+### Default Parameters
+```js
+{
+  "type": {
+    "name": "isosurface",
+    "params": {
+      "alpha": 1,
+      "useFog": true,
+      "highlightColor": 16737945,
+      "selectColor": 3407641,
+      "quality": "auto",
+      "doubleSided": false,
+      "flipSided": false,
+      "flatShaded": false,
+      "isoValue": {
+        "kind": "relative",
+        "stats": {
+          "min": 0,
+          "max": 0,
+          "mean": 0,
+          "sigma": 0
+        },
+        "relativeValue": 2
+      },
+      "sizeFactor": 1,
+      "lineSizeAttenuation": false,
+      "visuals": [
+        "solid"
+      ]
+    }
+  },
+  "colorTheme": {
+    "name": "uniform",
+    "params": {
+      "value": 13421772
+    }
+  },
+  "sizeTheme": {
+    "name": "uniform",
+    "params": {
+      "value": 1
+    }
+  }
+}
+```
+----------------------------
+## <a name="ms-plugin-focus-loci-on-select"></a>ms-plugin.focus-loci-on-select :: Root -> Behavior
+
+### Parameters
+- **minRadius**: Numeric value
+- **extraRadius**: Numeric value *(Value added to the boundning sphere radius of the Loci.)*
+
+### Default Parameters
+```js
+{
+  "minRadius": 10,
+  "extraRadius": 4
+}
+```
+----------------------------
+## <a name="ms-plugin-pdbe-structure-quality-report-prop"></a>ms-plugin.pdbe-structure-quality-report-prop :: Root -> Behavior
+
+### Parameters
+- **autoAttach**: true/false
+
+### Default Parameters
+```js
+{
+  "autoAttach": false
+}
+```
+----------------------------
+## <a name="ms-plugin-rcsb-assembly-symmetry-prop"></a>ms-plugin.rcsb-assembly-symmetry-prop :: Root -> Behavior
+
+### Parameters
+- **autoAttach**: true/false
+
+### Default Parameters
+```js
+{
+  "autoAttach": false
+}
+```
+----------------------------
+## <a name="ms-plugin-structure-animation"></a>ms-plugin.structure-animation :: Root -> Behavior
+
+### Parameters
+- **rotate**: true/false
+- **rotateValue**: Numeric value
+- **explode**: true/false
+- **explodeValue**: Numeric value
+
+### Default Parameters
+```js
+{
+  "rotate": false,
+  "rotateValue": 0,
+  "explode": false,
+  "explodeValue": 0
+}
+```
+----------------------------
+## <a name="ms-plugin-scene-labels"></a>ms-plugin.scene-labels :: Root -> Behavior
+
+### Parameters
+- **alpha**: Numeric value
+- **useFog**: true/false
+- **highlightColor**: Color as 0xrrggbb
+- **selectColor**: Color as 0xrrggbb
+- **quality**: One of 'custom', 'auto', 'highest', 'higher', 'high', 'medium', 'low', 'lower', 'lowest'
+- **fontFamily**: One of 'sans-serif', 'monospace', 'serif', 'cursive'
+- **fontQuality**: One of '0', '1', '2', '3', '4'
+- **fontStyle**: One of 'normal', 'italic', 'oblique'
+- **fontVariant**: One of 'normal', 'small-caps'
+- **fontWeight**: One of 'normal', 'bold'
+- **sizeFactor**: Numeric value
+- **borderWidth**: Numeric value
+- **borderColor**: Color as 0xrrggbb
+- **offsetX**: Numeric value
+- **offsetY**: Numeric value
+- **offsetZ**: Numeric value
+- **background**: true/false
+- **backgroundMargin**: Numeric value
+- **backgroundColor**: Color as 0xrrggbb
+- **backgroundOpacity**: Numeric value
+- **attachment**: One of 'bottom-left', 'bottom-center', 'bottom-right', 'middle-left', 'middle-center', 'middle-right', 'top-left', 'top-center', 'top-right'
+- **levels**: Array of 'structure', 'polymer', 'ligand'
+
+### Default Parameters
+```js
+{
+  "alpha": 1,
+  "useFog": true,
+  "highlightColor": 16737945,
+  "selectColor": 3407641,
+  "quality": "auto",
+  "fontFamily": "sans-serif",
+  "fontQuality": 3,
+  "fontStyle": "normal",
+  "fontVariant": "normal",
+  "fontWeight": "normal",
+  "sizeFactor": 1,
+  "borderWidth": 0,
+  "borderColor": 8421504,
+  "offsetX": 0,
+  "offsetY": 0,
+  "offsetZ": 0,
+  "background": true,
+  "backgroundMargin": 0.2,
+  "backgroundColor": 16775930,
+  "backgroundOpacity": 0.9,
+  "attachment": "middle-center",
+  "levels": []
+}
+```
+----------------------------

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 412 - 376
package-lock.json


+ 24 - 20
package.json

@@ -18,7 +18,7 @@
     "watch": "concurrently --kill-others \"npm:watch-ts\" \"npm:watch-extra\" \"npm:watch-webpack\"",
     "watch-ts": "tsc -watch",
     "watch-extra": "cpx \"src/**/*.{vert,frag,glsl,scss,woff,woff2,ttf,otf,eot,svg,html,gql}\" build/src/ --watch",
-    "build-webpack": "webpack --mode development",
+    "build-webpack": "webpack --mode production",
     "watch-webpack": "webpack -w --mode development",
     "model-server": "node build/src/servers/model/server.js",
     "model-server-watch": "nodemon --watch build/src build/src/servers/model/server.js"
@@ -54,6 +54,7 @@
       "mol-math($|/.*)": "<rootDir>/src/mol-math$1",
       "mol-model($|/.*)": "<rootDir>/src/mol-model$1",
       "mol-model-props($|/.*)": "<rootDir>/src/mol-model-props$1",
+      "mol-model-formats($|/.*)": "<rootDir>/src/mol-model-formats$1",
       "mol-plugin($|/.*)": "<rootDir>/src/mol-plugin$1",
       "mol-ql($|/.*)": "<rootDir>/src/mol-ql$1",
       "mol-repr($|/.*)": "<rootDir>/src/mol-repr$1",
@@ -74,16 +75,17 @@
   "author": "",
   "license": "MIT",
   "devDependencies": {
-    "@types/argparse": "^1.0.35",
+    "@types/argparse": "^1.0.36",
     "@types/benchmark": "^1.0.31",
     "@types/compression": "0.0.36",
-    "@types/express": "^4.16.0",
-    "@types/jest": "^23.3.12",
-    "@types/node": "^10.12.18",
-    "@types/node-fetch": "^2.1.4",
-    "@types/react": "^16.7.20",
-    "@types/react-dom": "^16.0.11",
+    "@types/express": "^4.16.1",
+    "@types/jest": "^24.0.9",
+    "@types/node": "^11.10.4",
+    "@types/node-fetch": "^2.1.6",
+    "@types/react": "^16.8.6",
+    "@types/react-dom": "^16.8.2",
     "@types/webgl2": "0.0.4",
+    "@types/swagger-ui-dist": "3.0.0",
     "benchmark": "^2.1.4",
     "circular-dependency-plugin": "^5.0.2",
     "concurrently": "^4.1.0",
@@ -94,23 +96,24 @@
     "glslify": "^7.0.0",
     "glslify-import": "^3.1.0",
     "glslify-loader": "^2.0.0",
-    "graphql-code-generator": "^0.15.2",
-    "graphql-codegen-typescript-template": "^0.15.2",
-    "jest": "^23.6.0",
+    "graphql-code-generator": "^0.18.0",
+    "graphql-codegen-time": "^0.18.0",
+    "graphql-codegen-typescript-template": "^0.18.0",
+    "jest": "^24.1.0",
     "jest-raw-loader": "^1.0.1",
     "mini-css-extract-plugin": "^0.5.0",
     "node-sass": "^4.11.0",
     "raw-loader": "^1.0.0",
-    "resolve-url-loader": "^3.0.0",
+    "resolve-url-loader": "^3.0.1",
     "sass-loader": "^7.1.0",
     "style-loader": "^0.23.1",
-    "ts-jest": "^23.10.5",
-    "tslint": "^5.12.1",
-    "typescript": "^3.2.4",
+    "ts-jest": "^24.0.0",
+    "tslint": "^5.13.1",
+    "typescript": "^3.3.3",
     "uglify-js": "^3.4.9",
     "util.promisify": "^1.0.0",
-    "webpack": "^4.28.4",
-    "webpack-cli": "^3.2.1"
+    "webpack": "^4.29.6",
+    "webpack-cli": "^3.2.3"
   },
   "dependencies": {
     "argparse": "^1.0.10",
@@ -119,8 +122,9 @@
     "graphql": "^14.1.1",
     "immutable": "^3.8.2",
     "node-fetch": "^2.3.0",
-    "react": "^16.7.0",
-    "react-dom": "^16.7.0",
-    "rxjs": "^6.3.3"
+    "react": "^16.8.4",
+    "react-dom": "^16.8.4",
+    "rxjs": "^6.4.0",
+    "swagger-ui-dist": "^3.21.0"
   }
 }

+ 30 - 0
src/apps/basic-wrapper/coloring.ts

@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CustomElementProperty } from 'mol-model-props/common/custom-element-property';
+import { Model, ElementIndex } from 'mol-model/structure';
+import { Color } from 'mol-util/color';
+
+export const StripedResidues = CustomElementProperty.create<number>({
+    isStatic: true,
+    name: 'basic-wrapper-residue-striping',
+    display: 'Residue Stripes',
+    getData(model: Model) {
+        const map = new Map<ElementIndex, number>();
+        const residueIndex = model.atomicHierarchy.residueAtomSegments.index;
+        for (let i = 0, _i = model.atomicHierarchy.atoms._rowCount; i < _i; i++) {
+            map.set(i as ElementIndex, residueIndex[i] % 2);
+        }
+        return map;
+    },
+    coloring: {
+        getColor(e) { return e === 0 ? Color(0xff0000) : Color(0x0000ff) },
+        defaultColor: Color(0x777777)
+    },
+    format(e) {
+        return e === 0 ? 'Odd stripe' : 'Even stripe'
+    }
+})

+ 128 - 0
src/apps/basic-wrapper/index.html

@@ -0,0 +1,128 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <title>Mol* Plugin Wrapper</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+                box-sizing: border-box;
+            }
+            #app {
+                position: absolute;
+                left: 160px;
+                top: 100px;
+                width: 600px;
+                height: 600px;
+                border: 1px solid #ccc;
+            }
+
+            #controls {
+                position: absolute;
+                width: 130px;
+                top: 10px;
+                left: 10px;
+            }
+
+            #controls > button {
+                display: block;
+                width: 100%;
+                text-align: left;
+            }
+
+            #controls > hr {
+                margin: 5px 0;
+            }
+
+            #controls > input, #controls > select {
+                width: 100%;
+                display: block;
+            }
+        </style>
+        <link rel="stylesheet" type="text/css" href="app.css" />
+        <script type="text/javascript" src="./index.js"></script>
+    </head>
+    <body>
+        <div id='controls'>
+            <h3>Source</h3>
+            <input type='text' id='url' placeholder='url' />
+            <input type='text' id='assemblyId' placeholder='assembly id' />
+            <select id='format'>
+                <option value='cif' selected>CIF</option>
+                <option value='pdb'>PDB</option>
+            </select>
+        </div>
+        <div id="app"></div>
+        <script>      
+            function $(id) { return document.getElementById(id); }
+        
+            var pdbId = '1grm', assemblyId= '1';
+            var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
+            var format = 'cif';
+            
+            $('url').value = url;
+            $('url').onchange = function (e) { url = e.target.value; }
+            $('assemblyId').value = assemblyId;
+            $('assemblyId').onchange = function (e) { assemblyId = e.target.value; }
+            $('format').value = format;
+            $('format').onchange = function (e) { format = e.target.value; }
+
+            // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
+            // var format = 'pdb';
+            // var assemblyId = 'deposited';
+
+            BasicMolStarWrapper.init('app' /** or document.getElementById('app') */);
+            BasicMolStarWrapper.setBackground(0xffffff);
+            BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId });
+            BasicMolStarWrapper.toggleSpin();
+
+            addControl('Load Asym Unit', () => BasicMolStarWrapper.load({ url: url, format: format }));
+            addControl('Load Assembly', () => BasicMolStarWrapper.load({ url: url, format: format, assemblyId: assemblyId }));
+
+            addSeparator();
+
+            addHeader('Camera');
+            addControl('Toggle Spin', () => BasicMolStarWrapper.toggleSpin());
+            
+            addSeparator();
+
+            addHeader('Animation');
+
+            // adjust this number to make the animation faster or slower
+            // requires to "restart" the animation if changed
+            BasicMolStarWrapper.animate.modelIndex.maxFPS = 30;
+
+            addControl('Play To End', () => BasicMolStarWrapper.animate.modelIndex.onceForward());
+            addControl('Play To Start', () => BasicMolStarWrapper.animate.modelIndex.onceBackward());
+            addControl('Play Palindrome', () => BasicMolStarWrapper.animate.modelIndex.palindrome());
+            addControl('Play Loop', () => BasicMolStarWrapper.animate.modelIndex.loop());
+            addControl('Stop', () => BasicMolStarWrapper.animate.modelIndex.stop());
+
+            addHeader('Misc');
+
+            addControl('Apply Stripes', () => BasicMolStarWrapper.coloring.applyStripes());
+
+            ////////////////////////////////////////////////////////
+
+            function addControl(label, action) {
+                var btn = document.createElement('button');
+                btn.onclick = action;
+                btn.innerText = label;
+                $('controls').appendChild(btn);
+            }
+
+            function addSeparator() {
+                var hr = document.createElement('hr');
+                $('controls').appendChild(hr);
+            }
+
+            function addHeader(header) {
+                var h = document.createElement('h3');
+                h.innerText = header;
+                $('controls').appendChild(h);
+            }
+        </script>
+    </body>
+</html>

+ 139 - 0
src/apps/basic-wrapper/index.ts

@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { createPlugin, DefaultPluginSpec } from 'mol-plugin';
+import './index.html'
+import { PluginContext } from 'mol-plugin/context';
+import { PluginCommands } from 'mol-plugin/command';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+import { Color } from 'mol-util/color';
+import { PluginStateObject as PSO } from 'mol-plugin/state/objects';
+import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
+import { StateBuilder } from 'mol-state';
+import { StripedResidues } from './coloring';
+require('mol-plugin/skin/light.scss')
+
+type SupportedFormats = 'cif' | 'pdb'
+type LoadParams = { url: string, format?: SupportedFormats, assemblyId?: string }
+
+class BasicWrapper {
+    plugin: PluginContext;
+
+    init(target: string | HTMLElement) {
+        this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
+            ...DefaultPluginSpec,
+            layout: {
+                initial: {
+                    isExpanded: false,
+                    showControls: false
+                }
+            }
+        });
+
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(StripedResidues.Descriptor.name, StripedResidues.colorTheme!);
+        this.plugin.lociLabels.addProvider(StripedResidues.labelProvider);
+        this.plugin.customModelProperties.register(StripedResidues.propertyProvider);
+    }
+
+    private download(b: StateBuilder.To<PSO.Root>, url: string) {
+        return b.apply(StateTransforms.Data.Download, { url, isBinary: false })
+    }
+
+    private parse(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
+        const parsed = format === 'cif'
+            ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+            : b.apply(StateTransforms.Model.TrajectoryFromPDB);
+
+        return parsed
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 })
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [StripedResidues.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+    }
+
+    private visual(visualRoot: StateBuilder.To<PSO.Molecule.Structure>) {
+        visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'cartoon'));
+        visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'ball-and-stick'));
+        visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'ball-and-stick', { alpha: 0.51 }));
+        visualRoot.apply(StateTransforms.Model.StructureComplexElement, { type: 'spheres' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(this.plugin, 'spacefill'));
+        return visualRoot;
+    }
+
+    private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' };
+    async load({ url, format = 'cif', assemblyId = '' }: LoadParams) {
+        let loadType: 'full' | 'update' = 'full';
+
+        const state = this.plugin.state.dataState;
+
+        if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
+            loadType = 'full';
+        } else if (this.loadedParams.url === url) {
+            if (state.select('asm').length > 0) loadType = 'update';
+        }
+
+        let tree: StateBuilder.Root;
+        if (loadType === 'full') {
+            await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
+            tree = state.build();
+            this.visual(this.parse(this.download(tree.toRoot(), url), format, assemblyId));
+        } else {
+            tree = state.build();
+            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+        }
+
+        await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
+        this.loadedParams = { url, format, assemblyId };
+        PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+    }
+
+    setBackground(color: number) {
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } });
+    }
+
+    toggleSpin() {
+        const trackball = this.plugin.canvas3d.props.trackball;
+        const spinning = trackball.spin;
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
+        if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+    }
+
+    animate = {
+        modelIndex: {
+            maxFPS: 8,
+            onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) },
+            onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) },
+            palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) },
+            loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) },
+            stop: () => this.plugin.state.animation.stop()
+        }
+    }
+
+    coloring = {
+        applyStripes: async () => {
+            const state = this.plugin.state.dataState;
+
+            const visuals = state.selectQ(q => q.ofTransformer(StateTransforms.Representation.StructureRepresentation3D));
+            const tree = state.build();
+            const colorTheme = { name: StripedResidues.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(StripedResidues.Descriptor.name).defaultValues };
+
+            for (const v of visuals) {
+                tree.to(v).update(old => ({ ...old, colorTheme }));
+            }
+
+            await PluginCommands.State.Update.dispatch(this.plugin, { state, tree });
+        }
+    }
+}
+
+(window as any).BasicMolStarWrapper = new BasicWrapper();

+ 2 - 2
src/apps/chem-comp-bond/create-table.ts

@@ -19,7 +19,7 @@ import { Database, Table, DatabaseCollection, Column } from 'mol-data/db'
 import CIF from 'mol-io/reader/cif'
 import { CifWriter } from 'mol-io/writer/cif'
 import { CCD_Schema } from 'mol-io/reader/cif/schema/ccd'
-import { difference } from 'mol-util/set'
+import { SetUtils } from 'mol-util/set'
 import { DefaultMap } from 'mol-util/map'
 
 export async function ensureAvailable(path: string, url: string) {
@@ -130,7 +130,7 @@ function checkAddingBondsFromPVCD(pvcd: DatabaseCollection<CCD_Schema>) {
                 for (let i = 0, il = parentIds.length; i < il; ++i) {
                     const entryBonds = addChemCompBondToSet(new Set<string>(), chem_comp_bond)
                     const entryAtoms = addChemCompAtomToSet(new Set<string>(), chem_comp_atom)
-                    const extraBonds = difference(ccbSetByParent.get(parentIds[i])!, entryBonds)
+                    const extraBonds = SetUtils.difference(ccbSetByParent.get(parentIds[i])!, entryBonds)
                     extraBonds.forEach(bk => {
                         const [a1, a2] = bk.split('|')
                         if (entryAtoms.has(a1) && entryAtoms.has(a2)) {

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

@@ -160,6 +160,7 @@ const COMMA_SEPARATED_LIST_FIELDS = [
     '_diffrn_source.pdbx_wavelength_list',
     '_em_diffraction.tilt_angle_list', // 20,40,50,55
     '_em_entity_assembly.entity_id_list',
+    '_entity.pdbx_description', // Endolysin,Beta-2 adrenergic receptor
     '_entity.pdbx_ec',
     '_entity_poly.pdbx_strand_id', // A,B
     '_pdbx_depui_entry_details.experimental_methods',

+ 69 - 0
src/apps/state-docs/index.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as _ from 'mol-plugin/state/transforms'
+import { StateTransformer, StateObject } from 'mol-state';
+import { StringBuilder } from 'mol-util';
+import * as fs from 'fs';
+import { paramsToMd } from './pd-to-md';
+import { PluginContext } from 'mol-plugin/context';
+import { ParamDefinition } from 'mol-util/param-definition';
+
+// force the transform to be evaluated
+_.StateTransforms.Data.Download.id
+
+// Empty plugin context
+const ctx = new PluginContext({
+    actions: [],
+    behaviors: []
+});
+
+const builder = StringBuilder.create();
+
+function typeToString(o: StateObject.Ctor[]) {
+    if (o.length === 0) return '()';
+    return o.map(o => o.name).join(' | ');
+}
+
+function writeTransformer(t: StateTransformer) {
+    StringBuilder.write(builder, `## <a name="${t.id.replace('.', '-')}"></a>${t.id} :: ${typeToString(t.definition.from)} -> ${typeToString(t.definition.to)}`);
+    StringBuilder.newline(builder);
+    if (t.definition.display.description) {
+        StringBuilder.write(builder, `*${t.definition.display.description}*`)
+        StringBuilder.newline(builder);
+    }
+    StringBuilder.newline(builder);
+    if (t.definition.params) {
+        const params = t.definition.params(void 0, ctx);
+        StringBuilder.write(builder, `### Parameters`);
+        StringBuilder.newline(builder);
+        StringBuilder.write(builder, paramsToMd(params));
+        StringBuilder.newline(builder);
+
+        StringBuilder.write(builder, `### Default Parameters`);
+        StringBuilder.newline(builder);
+        StringBuilder.write(builder, `\`\`\`js\n${JSON.stringify(ParamDefinition.getDefaultValues(params), null, 2)}\n\`\`\``);
+        StringBuilder.newline(builder);
+    }
+    StringBuilder.write(builder, '----------------------------')
+    StringBuilder.newline(builder);
+}
+
+const transformers = StateTransformer.getAll();
+
+StringBuilder.write(builder, '# Mol* Plugin State Transformer Reference');
+StringBuilder.newline(builder);
+StringBuilder.newline(builder);
+transformers.forEach(t => {
+    StringBuilder.write(builder, `* [${t.id}](#${t.id.replace('.', '-')})`);
+    StringBuilder.newline(builder);
+});
+StringBuilder.newline(builder);
+StringBuilder.write(builder, '----------------------------')
+StringBuilder.newline(builder);
+transformers.forEach(t => writeTransformer(t));
+
+fs.writeFileSync(`docs/state/transforms.md`, StringBuilder.getString(builder));

+ 75 - 0
src/apps/state-docs/pd-to-md.ts

@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ParamDefinition as PD } from 'mol-util/param-definition';
+
+export function paramsToMd(params: PD.Params) {
+    return getParams(params, 0);
+}
+
+function paramInfo(param: PD.Any, offset: number): string {
+    switch (param.type) {
+        case 'value': return 'Value';
+        case 'boolean': return 'true/false';
+        case 'number': return 'Numeric value';
+        case 'converted': return paramInfo(param.converted, offset);
+        case 'conditioned': return getParams(param.conditionParams, offset);
+        case 'multi-select': return `Array of ${oToS(param.options)}`;
+        case 'color': return 'Color as 0xrrggbb';
+        case 'color-scale': return `One of ${oToS(param.options)}`;
+        case 'vec3': return `3D vector [x, y, z]`;
+        case 'file': return `JavaScript File Handle`;
+        case 'select': return `One of ${oToS(param.options)}`;
+        case 'text': return 'String';
+        case 'interval': return `Interval [min, max]`;
+        case 'group': return `Object with:\n${getParams(param.params, offset + 2)}`;
+        case 'mapped': return `Object { name: string, params: object } where name+params are:\n${getMapped(param, offset + 2)}`;
+        case 'line-graph': return `A list of 2d vectors [xi, yi][]`;
+        case 'object-list': return `Array of\n${paramInfo(PD.Group(param.element), offset + 2)}`;
+        // TODO: support more languages
+        case 'script-expression': return `An expression in the specified language { language: 'mol-script', expressiong: string }`;
+        default:
+            const _: never = param;
+            console.warn(`${_} has no associated UI component`);
+            return '';
+    }
+}
+
+function oToS(options: [string, string][]) {
+    return options.map(o => `'${o[0]}'`).join(', ');
+}
+
+function offsetS(n: number) {
+    return new Array(n + 1).join(' ') + '- ';
+}
+
+function getMapped(param: PD.Mapped<any>, offset: number) {
+    let ret = '';
+    for (const [n] of param.select.options) {
+        ret += offsetS(offset);
+        ret += `**${n}**:\n`;
+        ret += paramInfo(param.map(n), offset + 2);
+        ret += '\n';
+    }
+    return ret;
+}
+
+function getParams(params: PD.Params, offset: number) {
+    let ret = '';
+    for (const k of Object.keys(params)) {
+        const param = params[k];
+        ret += offsetS(offset);
+        ret += `**${k}**${param.isOptional ? '?:' : ':'} ${paramInfo(param, offset)}`;
+        // if (param.defaultValue) {
+        //     ret += ` = ${JSON.stringify(param.defaultValue)}`;
+        // }
+        if (param.description) {
+            ret += ` *(${param.description})*`;
+        }
+        ret += '\n';
+    }
+    return ret;
+}

+ 4 - 3
src/apps/structure-info/model.ts

@@ -9,11 +9,12 @@ import * as argparse from 'argparse'
 require('util.promisify').shim();
 
 import { CifFrame } from 'mol-io/reader/cif'
-import { Model, Structure, StructureElement, Unit, Format, StructureProperties, UnitRing } from 'mol-model/structure'
+import { Model, Structure, StructureElement, Unit, StructureProperties, UnitRing } from 'mol-model/structure'
 // import { Run, Progress } from 'mol-task'
 import { OrderedSet } from 'mol-data/int';
 import { openCif, downloadCif } from './helpers';
 import { Vec3 } from 'mol-math/linear-algebra';
+import { trajectoryFromMmCIF } from 'mol-model-formats/structure/mmcif';
 
 
 async function downloadFromPdb(pdb: string) {
@@ -198,7 +199,7 @@ export function printModelStats(models: ReadonlyArray<Model>) {
 }
 
 export async function getModelsAndStructure(frame: CifFrame) {
-    const models = await Model.create(Format.mmCIF(frame)).run();
+    const models = await trajectoryFromMmCIF(frame).run();
     const structure = Structure.ofModel(models[0]);
     return { models, structure };
 }
@@ -247,7 +248,7 @@ interface Args {
     download?: string,
     file?: string,
 
-    models?:boolean,
+    models?: boolean,
     seq?: boolean,
     ihm?: boolean,
     units?: boolean,

+ 6 - 4
src/apps/structure-info/volume.ts

@@ -8,14 +8,16 @@ import * as fs from 'fs'
 import * as argparse from 'argparse'
 import * as util from 'util'
 
-import { VolumeData, parseDensityServerData, VolumeIsoValue } from 'mol-model/volume'
+import { VolumeData, VolumeIsoValue } from 'mol-model/volume'
 import { downloadCif } from './helpers'
 import CIF from 'mol-io/reader/cif'
 import { DensityServer_Data_Database } from 'mol-io/reader/cif/schema/density-server';
 import { Table } from 'mol-data/db';
 import { StringBuilder } from 'mol-util';
 import { Task } from 'mol-task';
-import { createVolumeIsosurface } from 'mol-repr/volume/isosurface-mesh';
+import { createVolumeIsosurfaceMesh } from 'mol-repr/volume/isosurface';
+import { createEmptyTheme } from 'mol-theme/theme';
+import { volumeFromDensityServerData } from 'mol-model-formats/volume/density-server';
 
 require('util.promisify').shim();
 const writeFileAsync = util.promisify(fs.writeFile);
@@ -25,7 +27,7 @@ type Volume = { source: DensityServer_Data_Database, volume: VolumeData }
 async function getVolume(url: string): Promise<Volume> {
     const cif = await downloadCif(url, true);
     const data = CIF.schema.densityServer(cif.blocks[1]);
-    return { source: data, volume: await parseDensityServerData(data).run() };
+    return { source: data, volume: await volumeFromDensityServerData(data).run() };
 }
 
 function print(data: Volume) {
@@ -38,7 +40,7 @@ function print(data: Volume) {
 }
 
 async function doMesh(data: Volume, filename: string) {
-    const mesh = await Task.create('', runtime => createVolumeIsosurface({ runtime }, data.volume, { isoValue: VolumeIsoValue.calcAbsolute(data.volume.dataStats, 1.5) } )).run();
+    const mesh = await Task.create('', runtime => createVolumeIsosurfaceMesh({ runtime }, data.volume, createEmptyTheme(), { isoValue: VolumeIsoValue.absolute(1.5) } )).run();
     console.log({ vc: mesh.vertexCount, tc: mesh.triangleCount });
 
     // Export the mesh in OBJ format.

+ 171 - 0
src/apps/viewer/extensions/jolecule.ts

@@ -0,0 +1,171 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { StateTree, StateBuilder, StateAction, State } from 'mol-state';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { createModelTree, complexRepresentation } from 'mol-plugin/state/actions/structure';
+import { PluginContext } from 'mol-plugin/context';
+import { PluginStateObject } from 'mol-plugin/state/objects';
+import { ParamDefinition } from 'mol-util/param-definition';
+import { PluginCommands } from 'mol-plugin/command';
+import { Vec3 } from 'mol-math/linear-algebra';
+import { PluginStateSnapshotManager } from 'mol-plugin/state/snapshots';
+import { MolScriptBuilder as MS } from 'mol-script/language/builder';
+import { Text } from 'mol-geo/geometry/text/text';
+import { UUID } from 'mol-util';
+import { ColorNames } from 'mol-util/color/tables';
+import { Camera } from 'mol-canvas3d/camera';
+import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+
+export const CreateJoleculeState = StateAction.build({
+    display: { name: 'Jolecule State Import' },
+    params: { id: ParamDefinition.Text('1mbo') },
+    from: PluginStateObject.Root
+})(async ({ ref, state, params }, plugin: PluginContext) => {
+    try {
+        const id = params.id.trim().toLowerCase();
+        const data = await plugin.runTask(plugin.fetch({ url: `https://jolecule.appspot.com/pdb/${id}.views.json`, type: 'json' })) as JoleculeSnapshot[];
+
+        data.sort((a, b) => a.order - b.order);
+
+        await PluginCommands.State.RemoveObject.dispatch(plugin, { state, ref });
+        plugin.state.snapshots.clear();
+
+        const template = createTemplate(plugin, state, id);
+        const snapshots = data.map((e, idx) => buildSnapshot(plugin, template, { e, idx, len: data.length }));
+        for (const s of snapshots) {
+            plugin.state.snapshots.add(s);
+        }
+
+        PluginCommands.State.Snapshots.Apply.dispatch(plugin, { id: snapshots[0].snapshot.id });
+    } catch (e) {
+        plugin.log.error(`Jolecule Failed: ${e}`);
+    }
+});
+
+interface JoleculeSnapshot {
+    order: number,
+    distances: { i_atom1: number, i_atom2: number }[],
+    labels: { i_atom: number, text: string }[],
+    camera: { up: Vec3, pos: Vec3, in: Vec3, slab: { z_front: number, z_back: number, zoom: number } },
+    selected: number[],
+    text: string
+}
+
+function createTemplate(plugin: PluginContext, state: State, id: string) {
+    const b = new StateBuilder.Root(state.tree);
+    const data = b.toRoot().apply(StateTransforms.Data.Download, { url: `https://www.ebi.ac.uk/pdbe/static/entry/${id}_updated.cif` }, { props: { isGhost: true }});
+    const model = createModelTree(data, 'cif');
+    const structure = model.apply(StateTransforms.Model.StructureFromModel, {});
+    complexRepresentation(plugin, structure, { hideWater: true });
+    return { tree: b.getTree(), structure: structure.ref };
+}
+
+const labelOptions: ParamDefinition.Values<Text.Params> = {
+    ...ParamDefinition.getDefaultValues(Text.Params),
+    tether: true,
+    sizeFactor: 1.3,
+    attachment: 'bottom-right',
+    offsetZ: 10,
+    background: true,
+    backgroundMargin: 0.2,
+    backgroundColor: ColorNames.skyblue,
+    backgroundOpacity: 0.9
+}
+
+// const distanceLabelOptions = {
+//     ...ParamDefinition.getDefaultValues(Text.Params),
+//     sizeFactor: 1,
+//     offsetX: 0,
+//     offsetY: 0,
+//     offsetZ: 10,
+//     background: true,
+//     backgroundMargin: 0.2,
+//     backgroundColor: ColorNames.snow,
+//     backgroundOpacity: 0.9
+// }
+
+function buildSnapshot(plugin: PluginContext, template: { tree: StateTree, structure: string }, params: { e: JoleculeSnapshot, idx: number, len: number }): PluginStateSnapshotManager.Entry {
+    const b = new StateBuilder.Root(template.tree);
+
+    let i = 0;
+    for (const l of params.e.labels) {
+        const query = createQuery([l.i_atom]);
+        const group = b.to(template.structure)
+            .group(StateTransforms.Misc.CreateGroup, { label: `Label ${++i}` });
+
+        group
+            .apply(StateTransforms.Model.StructureSelection, { query, label: 'Atom' })
+            .apply(StateTransforms.Representation.StructureLabels3D, {
+                target: { name: 'static-text', params: { value: l.text || '' } },
+                options: labelOptions
+            });
+
+        group
+            .apply(StateTransforms.Model.StructureSelection, { query: MS.struct.modifier.wholeResidues([query]), label: 'Residue' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'ball-and-stick', {  }));
+    }
+    if (params.e.selected && params.e.selected.length > 0) {
+        b.to(template.structure)
+            .apply(StateTransforms.Model.StructureSelection, { query: createQuery(params.e.selected), label: `Selected` })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsStatic(plugin, 'ball-and-stick'));
+    }
+    // TODO
+    // for (const l of params.e.distances) {
+    //     b.to('structure')
+    //         .apply(StateTransforms.Model.StructureSelection, { query: createQuery([l.i_atom1, l.i_atom2]), label: `Distance ${++i}` })
+    //         .apply(StateTransforms.Representation.StructureLabels3D, {
+    //             target: { name: 'static-text', params: { value: l. || '' } },
+    //             options: labelOptions
+    //         });
+    // }
+    return PluginStateSnapshotManager.Entry({
+        id: UUID.create22(),
+        data: { tree: StateTree.toJSON(b.getTree()) },
+        camera: {
+            current: getCameraSnapshot(params.e.camera),
+            transitionStyle: 'animate',
+            transitionDurationInMs: 350
+        }
+    }, {
+        name:  params.e.text
+    });
+}
+
+function getCameraSnapshot(e: JoleculeSnapshot['camera']): Camera.Snapshot {
+    const direction = Vec3.sub(Vec3.zero(), e.pos, e.in);
+    Vec3.normalize(direction, direction);
+    const up = Vec3.sub(Vec3.zero(), e.pos, e.up);
+    Vec3.normalize(up, up);
+
+    const s: Camera.Snapshot = {
+        mode: 'perspective',
+        position: Vec3.scaleAndAdd(Vec3.zero(), e.pos, direction, e.slab.zoom),
+        target: e.pos,
+        direction,
+        up,
+        near: e.slab.zoom + e.slab.z_front,
+        far: e.slab.zoom + e.slab.z_back,
+        fogNear: e.slab.zoom + e.slab.z_front,
+        fogFar: e.slab.zoom + e.slab.z_back,
+        fov: Math.PI / 4,
+        zoom: 1
+    };
+    return s;
+}
+
+function createQuery(atomIndices: number[]) {
+    if (atomIndices.length === 0) return MS.struct.generator.empty();
+
+    return MS.struct.generator.atomGroups({
+        'atom-test': atomIndices.length === 1
+            ? MS.core.rel.eq([MS.struct.atomProperty.core.sourceIndex(), atomIndices[0]])
+            : MS.core.set.has([MS.set.apply(null, atomIndices), MS.struct.atomProperty.core.sourceIndex()]),
+        'group-by': 0
+    });
+}

+ 8 - 1
src/apps/viewer/index.html

@@ -25,11 +25,18 @@
             button {
                 padding: 2px;
             }
+            #app {
+                position: absolute;
+                left: 100px;
+                top: 100px;
+                width: 800px;
+                height: 600px;
+            }
         </style>
         <link rel="stylesheet" type="text/css" href="app.css" />
     </head>
     <body>
-        <div id="app" style="position: absolute; width: 100%; height: 100%"></div>
+        <div id="app"></div>
         <script type="text/javascript" src="./index.js"></script>
     </body>
 </html>

+ 46 - 3
src/apps/viewer/index.ts

@@ -1,11 +1,54 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import { createPlugin } from 'mol-plugin';
+import { createPlugin, DefaultPluginSpec } from 'mol-plugin';
 import './index.html'
+import { PluginContext } from 'mol-plugin/context';
+import { PluginCommands } from 'mol-plugin/command';
+import { PluginSpec } from 'mol-plugin/spec';
+import { CreateJoleculeState } from './extensions/jolecule';
 require('mol-plugin/skin/light.scss')
 
-createPlugin(document.getElementById('app')!);
+function getParam(name: string, regex: string): string {
+    let r = new RegExp(`${name}=(${regex})[&]?`, 'i');
+    return decodeURIComponent(((window.location.search || '').match(r) || [])[1] || '');
+}
+
+const hideControls = getParam('hide-controls', `[^&]+`) === '1';
+
+function init() {
+    const spec: PluginSpec = {
+        actions: [...DefaultPluginSpec.actions, PluginSpec.Action(CreateJoleculeState)],
+        behaviors: [...DefaultPluginSpec.behaviors],
+        animations: [...DefaultPluginSpec.animations || []],
+        layout: {
+            initial: {
+                isExpanded: true,
+                showControls: !hideControls
+            }
+        }
+    };
+    const plugin = createPlugin(document.getElementById('app')!, spec);
+    trySetSnapshot(plugin);
+}
+
+async function trySetSnapshot(ctx: PluginContext) {
+    try {
+        const snapshotUrl = getParam('snapshot-url', `[^&]+`);
+        const snapshotId = getParam('snapshot-id', `[^&]+`);
+        if (!snapshotUrl && !snapshotId) return;
+        // TODO parametrize the server
+        const url = snapshotId
+            ? `https://webchem.ncbr.muni.cz/molstar-state/get/${snapshotId}`
+            : snapshotUrl;
+        await PluginCommands.State.Snapshots.Fetch.dispatch(ctx, { url })
+    } catch (e) {
+        ctx.log.error('Failed to load snapshot.');
+        console.warn('Failed to load snapshot', e);
+    }
+}
+
+init();

+ 72 - 0
src/examples/proteopedia-wrapper/annotation.ts

@@ -0,0 +1,72 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { CustomElementProperty } from 'mol-model-props/common/custom-element-property';
+import { Model, ElementIndex, ResidueIndex } from 'mol-model/structure';
+import { Color } from 'mol-util/color';
+
+const EvolutionaryConservationPalette: Color[] = [
+    [255, 255, 129], // insufficient
+    [160, 37, 96], // 9
+    [240, 125, 171],
+    [250, 201, 222],
+    [252, 237, 244],
+    [255, 255, 255],
+    [234, 255, 255],
+    [215, 255, 255],
+    [140, 255, 255],
+    [16, 200, 209] // 1
+].reverse().map(([r, g, b]) => Color.fromRgb(r, g, b));
+const EvolutionaryConservationDefaultColor = Color(0x999999);
+
+export const EvolutionaryConservation = CustomElementProperty.create<number>({
+    isStatic: true,
+    name: 'proteopedia-wrapper-evolutionary-conservation',
+    display: 'Evolutionary Conservation',
+    async getData(model: Model) {
+        const id = model.label.toLowerCase();
+        const req = await fetch(`https://proteopedia.org/cgi-bin/cnsrf?${id}`);
+        const json = await req.json();
+        const annotations = (json && json.residueAnnotations) || [];
+
+        const conservationMap = new Map<string, number>();
+
+        for (const e of annotations) {
+            for (const r of e.ids) {
+                conservationMap.set(r, e.annotation);
+            }
+        }
+
+        const map = new Map<ElementIndex, number>();
+
+        const { _rowCount: residueCount } = model.atomicHierarchy.residues;
+        const { offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments;
+        const chainIndex = model.atomicHierarchy.chainAtomSegments.index;
+
+        for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) {
+            const cI = chainIndex[residueOffsets[rI]];
+            const key = `${model.atomicHierarchy.chains.auth_asym_id.value(cI)} ${model.atomicHierarchy.residues.auth_seq_id.value(rI)}`;
+            if (!conservationMap.has(key)) continue;
+            const ann = conservationMap.get(key)!;
+            for (let aI = residueOffsets[rI]; aI < residueOffsets[rI + 1]; aI++) {
+                map.set(aI, ann);
+            }
+        }
+
+        return map;
+    },
+    coloring: {
+        getColor(e: number) {
+            if (e < 1 || e > 10) return EvolutionaryConservationDefaultColor;
+            return EvolutionaryConservationPalette[e - 1];
+        },
+        defaultColor: EvolutionaryConservationDefaultColor
+    },
+    format(e) {
+        if (e === 10) return `Evolutionary Conservation: InsufficientData`;
+        return e ? `Evolutionary Conservation: ${e}` : void 0;
+    }
+});

+ 7 - 0
src/examples/proteopedia-wrapper/changelog.md

@@ -0,0 +1,7 @@
+== v2.0 ==
+
+* Changed how state saving works.
+
+== v1.0 ==
+
+* Initial version.

+ 100 - 0
src/examples/proteopedia-wrapper/helpers.ts

@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { ResidueIndex, Model } from 'mol-model/structure';
+import { BuiltInStructureRepresentationsName } from 'mol-repr/structure/registry';
+import { BuiltInColorThemeName } from 'mol-theme/color';
+import { AminoAcidNames } from 'mol-model/structure/model/types';
+import { PluginContext } from 'mol-plugin/context';
+
+export interface ModelInfo {
+    hetResidues: { name: string, indices: ResidueIndex[] }[],
+    assemblies: { id: string, details: string, isPreferred: boolean }[],
+    preferredAssemblyId: string | undefined
+}
+
+export namespace ModelInfo {
+    async function getPreferredAssembly(ctx: PluginContext, model: Model) {
+        if (model.label.length <= 3) return void 0;
+        try {
+            const id = model.label.toLowerCase();
+            const src = await ctx.runTask(ctx.fetch({ url: `https://www.ebi.ac.uk/pdbe/api/pdb/entry/summary/${id}` })) as string;
+            const json = JSON.parse(src);
+            const data = json && json[id];
+
+            const assemblies = data[0] && data[0].assemblies;
+            if (!assemblies || !assemblies.length) return void 0;
+
+            for (const asm of assemblies) {
+                if (asm.preferred) {
+                    return asm.assembly_id;
+                }
+            }
+            return void 0;
+        } catch (e) {
+            console.warn('getPreferredAssembly', e);
+        }
+    }
+
+    export async function get(ctx: PluginContext, model: Model, checkPreferred: boolean): Promise<ModelInfo> {
+        const { _rowCount: residueCount } = model.atomicHierarchy.residues;
+        const { offsets: residueOffsets } = model.atomicHierarchy.residueAtomSegments;
+        const chainIndex = model.atomicHierarchy.chainAtomSegments.index;
+        // const resn = SP.residue.label_comp_id, entType = SP.entity.type;
+
+        const pref = checkPreferred
+            ? getPreferredAssembly(ctx, model)
+            : void 0;
+
+        const hetResidues: ModelInfo['hetResidues'] = [];
+        const hetMap = new Map<string, ModelInfo['hetResidues'][0]>();
+
+        for (let rI = 0 as ResidueIndex; rI < residueCount; rI++) {
+            const comp_id = model.atomicHierarchy.residues.label_comp_id.value(rI);
+            if (AminoAcidNames.has(comp_id)) continue;
+            const mod_parent = model.properties.modifiedResidues.parentId.get(comp_id);
+            if (mod_parent && AminoAcidNames.has(mod_parent)) continue;
+
+            const cI = chainIndex[residueOffsets[rI]];
+            const eI = model.atomicHierarchy.index.getEntityFromChain(cI);
+            if (model.entities.data.type.value(eI) === 'water') continue;
+
+            let lig = hetMap.get(comp_id);
+            if (!lig) {
+                lig = { name: comp_id, indices: [] };
+                hetResidues.push(lig);
+                hetMap.set(comp_id, lig);
+            }
+            lig.indices.push(rI);
+        }
+
+        const preferredAssemblyId = await pref;
+
+        return {
+            hetResidues: hetResidues,
+            assemblies: model.symmetry.assemblies.map(a => ({ id: a.id, details: a.details, isPreferred: a.id === preferredAssemblyId })),
+            preferredAssemblyId
+        };
+    }
+}
+
+export type SupportedFormats = 'cif' | 'pdb'
+export interface LoadParams {
+    url: string,
+    format?: SupportedFormats,
+    assemblyId?: string,
+    representationStyle?: RepresentationStyle
+}
+
+export interface RepresentationStyle {
+    sequence?: RepresentationStyle.Entry,
+    hetGroups?: RepresentationStyle.Entry,
+    water?: RepresentationStyle.Entry
+}
+
+export namespace RepresentationStyle {
+    export type Entry = { kind?: BuiltInStructureRepresentationsName, coloring?: BuiltInColorThemeName }
+}

+ 155 - 0
src/examples/proteopedia-wrapper/index.html

@@ -0,0 +1,155 @@
+<!DOCTYPE html>
+<html lang="en">
+    <head>
+        <meta charset="utf-8" />
+        <meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
+        <title>Mol* Proteopedia Wrapper</title>
+        <style>
+            * {
+                margin: 0;
+                padding: 0;
+                box-sizing: border-box;
+            }
+            #app {
+                position: absolute;
+                left: 160px;
+                top: 100px;
+                width: 600px;
+                height: 600px;
+                border: 1px solid #ccc;
+            }
+
+            #controls {
+                position: absolute;
+                width: 130px;
+                top: 10px;
+                left: 10px;
+            }
+
+            #controls > button {
+                display: block;
+                width: 100%;
+                text-align: left;
+            }
+
+            #controls > hr {
+                margin: 5px 0;
+            }
+
+            #controls > input, #controls > select {
+                width: 100%;
+                display: block;
+            }
+        </style>
+        <link rel="stylesheet" type="text/css" href="app.css" />
+        <script type="text/javascript" src="./index.js"></script>
+    </head>
+    <body>
+        <div id='controls'>
+            <h3>Source</h3>
+            <input type='text' id='url' placeholder='url' style='width: 400px' />
+            <input type='text' id='assemblyId' placeholder='assembly id' />
+            <select id='format'>
+                <option value='cif' selected>CIF</option>
+                <option value='pdb'>PDB</option>
+            </select>
+        </div>
+        <div id="app"></div>
+        <script>  
+            // create an instance of the plugin
+            var PluginWrapper = new MolStarProteopediaWrapper();
+
+            console.log('Wrapper version', MolStarProteopediaWrapper.VERSION_MAJOR);
+
+            function $(id) { return document.getElementById(id); }
+        
+            var pdbId = '1eve', assemblyId= 'preferred';
+            var url = 'https://www.ebi.ac.uk/pdbe/static/entry/' + pdbId + '_updated.cif';
+            var format = 'cif';
+            
+            $('url').value = url;
+            $('url').onchange = function (e) { url = e.target.value; }
+            $('assemblyId').value = assemblyId;
+            $('assemblyId').onchange = function (e) { assemblyId = e.target.value; }
+            $('format').value = format;
+            $('format').onchange = function (e) { format = e.target.value; }
+
+            // var url = 'https://www.ebi.ac.uk/pdbe/entry-files/pdb' + pdbId + '.ent';
+            // var format = 'pdb';
+            // var assemblyId = 'deposited';
+
+            PluginWrapper.init('app' /** or document.getElementById('app') */);
+            PluginWrapper.setBackground(0xffffff);
+            PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId });
+            PluginWrapper.toggleSpin();
+
+            PluginWrapper.events.modelInfo.subscribe(function (info) {
+                console.log('Model Info', info);
+            });
+
+            addControl('Load Asym Unit', () => PluginWrapper.load({ url: url, format: format }));
+            addControl('Load Assembly', () => PluginWrapper.load({ url: url, format: format, assemblyId: assemblyId }));
+
+            addSeparator();
+
+            addHeader('Camera');
+            addControl('Toggle Spin', () => PluginWrapper.toggleSpin());
+            
+            addSeparator();
+
+            addHeader('Animation');
+
+            // adjust this number to make the animation faster or slower
+            // requires to "restart" the animation if changed
+            PluginWrapper.animate.modelIndex.maxFPS = 30;
+
+            addControl('Play To End', () => PluginWrapper.animate.modelIndex.onceForward());
+            addControl('Play To Start', () => PluginWrapper.animate.modelIndex.onceBackward());
+            addControl('Play Palindrome', () => PluginWrapper.animate.modelIndex.palindrome());
+            addControl('Play Loop', () => PluginWrapper.animate.modelIndex.loop());
+            addControl('Stop', () => PluginWrapper.animate.modelIndex.stop());
+
+            addSeparator();
+            addHeader('Misc');
+
+            addControl('Apply Evo Cons', () => PluginWrapper.coloring.evolutionaryConservation());
+            addControl('Default Visuals', () => PluginWrapper.updateStyle());
+
+            addSeparator();
+            addHeader('State');
+
+            var snapshot;
+            addControl('Create Snapshot', () => {
+                snapshot = PluginWrapper.snapshot.get();
+                // could use JSON.stringify(snapshot) and upload the data
+            });
+            addControl('Apply Snapshot', () => {
+                if (!snapshot) return;
+                PluginWrapper.snapshot.set(snapshot);
+
+                // or download snapshot using fetch or ajax or whatever
+                // or PluginWrapper.snapshot.download(url);
+            });
+
+            ////////////////////////////////////////////////////////
+
+            function addControl(label, action) {
+                var btn = document.createElement('button');
+                btn.onclick = action;
+                btn.innerText = label;
+                $('controls').appendChild(btn);
+            }
+
+            function addSeparator() {
+                var hr = document.createElement('hr');
+                $('controls').appendChild(hr);
+            }
+
+            function addHeader(header) {
+                var h = document.createElement('h3');
+                h.innerText = header;
+                $('controls').appendChild(h);
+            }
+        </script>
+    </body>
+</html>

+ 225 - 0
src/examples/proteopedia-wrapper/index.ts

@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { createPlugin, DefaultPluginSpec } from 'mol-plugin';
+import './index.html'
+import { PluginContext } from 'mol-plugin/context';
+import { PluginCommands } from 'mol-plugin/command';
+import { StateTransforms } from 'mol-plugin/state/transforms';
+import { StructureRepresentation3DHelpers } from 'mol-plugin/state/transforms/representation';
+import { Color } from 'mol-util/color';
+import { PluginStateObject as PSO, PluginStateObject } from 'mol-plugin/state/objects';
+import { AnimateModelIndex } from 'mol-plugin/state/animation/built-in';
+import { StateBuilder, StateObject } from 'mol-state';
+import { EvolutionaryConservation } from './annotation';
+import { LoadParams, SupportedFormats, RepresentationStyle, ModelInfo } from './helpers';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
+import { ControlsWrapper } from './ui/controls';
+import { PluginState } from 'mol-plugin/state';
+require('mol-plugin/skin/light.scss')
+
+class MolStarProteopediaWrapper {
+    static VERSION_MAJOR = 2;
+    static VERSION_MINOR = 0;
+
+    private _ev = RxEventHelper.create();
+
+    readonly events = {
+        modelInfo: this._ev<ModelInfo>()
+    };
+
+    plugin: PluginContext;
+
+    init(target: string | HTMLElement) {
+        this.plugin = createPlugin(typeof target === 'string' ? document.getElementById(target)! : target, {
+            ...DefaultPluginSpec,
+            layout: {
+                initial: {
+                    isExpanded: false,
+                    showControls: false
+                },
+                controls: {
+                    right: ControlsWrapper
+                }
+            }
+        });
+
+        this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.add(EvolutionaryConservation.Descriptor.name, EvolutionaryConservation.colorTheme!);
+        this.plugin.lociLabels.addProvider(EvolutionaryConservation.labelProvider);
+        this.plugin.customModelProperties.register(EvolutionaryConservation.propertyProvider);
+    }
+
+    get state() {
+        return this.plugin.state.dataState;
+    }
+
+    private download(b: StateBuilder.To<PSO.Root>, url: string) {
+        return b.apply(StateTransforms.Data.Download, { url, isBinary: false })
+    }
+
+    private model(b: StateBuilder.To<PSO.Data.Binary | PSO.Data.String>, format: SupportedFormats, assemblyId: string) {
+        const parsed = format === 'cif'
+            ? b.apply(StateTransforms.Data.ParseCif).apply(StateTransforms.Model.TrajectoryFromMmCif)
+            : b.apply(StateTransforms.Model.TrajectoryFromPDB);
+
+        return parsed
+            .apply(StateTransforms.Model.ModelFromTrajectory, { modelIndex: 0 }, { ref: 'model' });
+    }
+
+    private structure(assemblyId: string) {
+        const model = this.state.build().to('model');
+
+        return model
+            .apply(StateTransforms.Model.CustomModelProperties, { properties: [EvolutionaryConservation.Descriptor.name] }, { ref: 'props', props: { isGhost: false } })
+            .apply(StateTransforms.Model.StructureAssemblyFromModel, { id: assemblyId || 'deposited' }, { ref: 'asm' });
+    }
+
+    private visual(ref: string, style?: RepresentationStyle) {
+        const structure = this.getObj<PluginStateObject.Molecule.Structure>(ref);
+        if (!structure) return;
+
+        const root = this.state.build().to(ref);
+
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-sequence' }, { ref: 'sequence' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                    (style && style.sequence && style.sequence.kind) || 'cartoon',
+                    (style && style.sequence && style.sequence.coloring) || 'unit-index', structure),
+                    { ref: 'sequence-visual' });
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'atomic-het' }, { ref: 'het' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                    (style && style.hetGroups && style.hetGroups.kind) || 'ball-and-stick',
+                    (style && style.hetGroups && style.hetGroups.coloring), structure),
+                    { ref: 'het-visual' });
+        root.apply(StateTransforms.Model.StructureComplexElement, { type: 'water' }, { ref: 'water' })
+            .apply(StateTransforms.Representation.StructureRepresentation3D,
+                StructureRepresentation3DHelpers.getDefaultParamsWithTheme(this.plugin,
+                    (style && style.water && style.water.kind) || 'ball-and-stick',
+                    (style && style.water && style.water.coloring), structure, { alpha: 0.51 }),
+                    { ref: 'water-visual' });
+
+        return root;
+    }
+
+    private getObj<T extends StateObject>(ref: string): T['data'] {
+        const state = this.state;
+        const cell = state.select(ref)[0];
+        if (!cell || !cell.obj) return void 0;
+        return (cell.obj as T).data;
+    }
+
+    private async doInfo(checkPreferredAssembly: boolean) {
+        const model = this.getObj<PluginStateObject.Molecule.Model>('model');
+        if (!model) return;
+
+        const info = await ModelInfo.get(this.plugin, model, checkPreferredAssembly)
+        this.events.modelInfo.next(info);
+        return info;
+    }
+
+    private applyState(tree: StateBuilder) {
+        return PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
+    }
+
+    private loadedParams: LoadParams = { url: '', format: 'cif', assemblyId: '' };
+    async load({ url, format = 'cif', assemblyId = '', representationStyle }: LoadParams) {
+        let loadType: 'full' | 'update' = 'full';
+
+        const state = this.plugin.state.dataState;
+
+        if (this.loadedParams.url !== url || this.loadedParams.format !== format) {
+            loadType = 'full';
+        } else if (this.loadedParams.url === url) {
+            if (state.select('asm').length > 0) loadType = 'update';
+        }
+
+        if (loadType === 'full') {
+            await PluginCommands.State.RemoveObject.dispatch(this.plugin, { state, ref: state.tree.root.ref });
+            const modelTree = this.model(this.download(state.build().toRoot(), url), format, assemblyId);
+            await this.applyState(modelTree);
+            const info = await this.doInfo(true);
+            const structureTree = this.structure((assemblyId === 'preferred' && info && info.preferredAssemblyId) || assemblyId);
+            await this.applyState(structureTree);
+        } else {
+            const tree = state.build();
+            tree.to('asm').update(StateTransforms.Model.StructureAssemblyFromModel, p => ({ ...p, id: assemblyId || 'deposited' }));
+            await this.applyState(tree);
+        }
+
+        await this.updateStyle(representationStyle);
+
+        this.loadedParams = { url, format, assemblyId };
+        PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+    }
+
+    async updateStyle(style?: RepresentationStyle) {
+        const tree = this.visual('asm', style);
+        if (!tree) return;
+        await PluginCommands.State.Update.dispatch(this.plugin, { state: this.plugin.state.dataState, tree });
+    }
+
+    setBackground(color: number) {
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { backgroundColor: Color(color) } });
+    }
+
+    toggleSpin() {
+        const trackball = this.plugin.canvas3d.props.trackball;
+        const spinning = trackball.spin;
+        PluginCommands.Canvas3D.SetSettings.dispatch(this.plugin, { settings: { trackball: { ...trackball, spin: !trackball.spin } } });
+        if (!spinning) PluginCommands.Camera.Reset.dispatch(this.plugin, { });
+    }
+
+    animate = {
+        modelIndex: {
+            maxFPS: 8,
+            onceForward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'forward' } } }) },
+            onceBackward: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'once', params: { direction: 'backward' } } }) },
+            palindrome: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'palindrome', params: {} } }) },
+            loop: () => { this.plugin.state.animation.play(AnimateModelIndex, { maxFPS: Math.max(0.5, this.animate.modelIndex.maxFPS | 0), mode: { name: 'loop', params: {} } }) },
+            stop: () => this.plugin.state.animation.stop()
+        }
+    }
+
+    coloring = {
+        evolutionaryConservation: async () => {
+            await this.updateStyle({ sequence: { kind: 'spacefill' } });
+
+            const state = this.state;
+
+            // const visuals = state.selectQ(q => q.ofType(PluginStateObject.Molecule.Structure.Representation3D).filter(c => c.transform.transformer === StateTransforms.Representation.StructureRepresentation3D));
+            const tree = state.build();
+            const colorTheme = { name: EvolutionaryConservation.Descriptor.name, params: this.plugin.structureRepresentation.themeCtx.colorThemeRegistry.get(EvolutionaryConservation.Descriptor.name).defaultValues };
+
+            tree.to('sequence-visual').update(StateTransforms.Representation.StructureRepresentation3D, old => ({ ...old, colorTheme }));
+            // for (const v of visuals) {
+            // }
+
+            await PluginCommands.State.Update.dispatch(this.plugin, { state, tree });
+        }
+    }
+
+    snapshot = {
+        get: () => {
+            return this.plugin.state.getSnapshot();
+        },
+        set: (snapshot: PluginState.Snapshot) => {
+            return this.plugin.state.setSnapshot(snapshot);
+        },
+        download: async (url: string) => {
+            try {
+                const data = await this.plugin.runTask(this.plugin.fetch({ url }));
+                const snapshot = JSON.parse(data);
+                await this.plugin.state.setSnapshot(snapshot);
+            } catch (e) {
+                console.log(e);
+            }
+        }
+
+    }
+}
+
+(window as any).MolStarProteopediaWrapper = MolStarProteopediaWrapper;

+ 21 - 0
src/examples/proteopedia-wrapper/ui/controls.tsx

@@ -0,0 +1,21 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import * as React from 'react';
+import { PluginUIComponent } from 'mol-plugin/ui/base';
+import { CurrentObject } from 'mol-plugin/ui/plugin';
+import { AnimationControls } from 'mol-plugin/ui/state/animation';
+import { CameraSnapshots } from 'mol-plugin/ui/camera';
+
+export class ControlsWrapper extends PluginUIComponent {
+    render() {
+        return <div className='msp-scrollable-container msp-right-controls'>
+            <CurrentObject />
+            <AnimationControls />
+            <CameraSnapshots />
+        </div>;
+    }
+}

+ 44 - 20
src/mol-canvas3d/canvas3d.ts

@@ -8,10 +8,9 @@ import { BehaviorSubject, Subscription } from 'rxjs';
 import { now } from 'mol-util/now';
 
 import { Vec3 } from 'mol-math/linear-algebra'
-import InputObserver from 'mol-util/input/input-observer'
-import * as SetUtils from 'mol-util/set'
+import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer'
 import Renderer, { RendererStats } from 'mol-gl/renderer'
-import { RenderObject } from 'mol-gl/render-object'
+import { GraphicsRenderObject } from 'mol-gl/render-object'
 
 import { TrackballControls, TrackballControlsParams } from './controls/trackball'
 import { Viewport } from './camera/util'
@@ -29,6 +28,8 @@ import { Camera } from './camera';
 import { ParamDefinition as PD } from 'mol-util/param-definition';
 import { BoundingSphereHelper, DebugHelperParams } from './helper/bounding-sphere-helper';
 import { decodeFloatRGB } from 'mol-util/float-packing';
+import { SetUtils } from 'mol-util/set';
+import { Canvas3dInteractionHelper } from './helper/interaction-events';
 
 export const Canvas3DParams = {
     // TODO: FPS cap?
@@ -51,7 +52,7 @@ interface Canvas3D {
 
     add: (repr: Representation.Any) => void
     remove: (repr: Representation.Any) => void
-    update: () => void
+    update: (repr?: Representation.Any, keepBoundingSphere?: boolean) => void
     clear: () => void
 
     // draw: (force?: boolean) => void
@@ -59,8 +60,8 @@ interface Canvas3D {
     animate: () => void
     pick: () => void
     identify: (x: number, y: number) => Promise<PickingId | undefined>
-    mark: (loci: Loci, action: MarkerAction, repr?: Representation.Any) => void
-    getLoci: (pickingId: PickingId) => { loci: Loci, repr?: Representation.Any }
+    mark: (loci: Representation.Loci, action: MarkerAction) => void
+    getLoci: (pickingId: PickingId) => Representation.Loci
 
     readonly didDraw: BehaviorSubject<now.Timestamp>
 
@@ -76,14 +77,19 @@ interface Canvas3D {
     readonly props: Canvas3DProps
     readonly input: InputObserver
     readonly stats: RendererStats
+    readonly interaction: Canvas3dInteractionHelper['events']
+
     dispose: () => void
 }
 
 namespace Canvas3D {
+    export interface HighlightEvent { current: Representation.Loci, prev: Representation.Loci, modifiers?: ModifiersKeys }
+    export interface ClickEvent { current: Representation.Loci, buttons: ButtonsType, modifiers: ModifiersKeys }
+
     export function create(canvas: HTMLCanvasElement, container: Element, props: Partial<Canvas3DProps> = {}): Canvas3D {
         const p = { ...PD.getDefaultValues(Canvas3DParams), ...props }
 
-        const reprRenderObjects = new Map<Representation.Any, Set<RenderObject>>()
+        const reprRenderObjects = new Map<Representation.Any, Set<GraphicsRenderObject>>()
         const reprUpdatedSubscriptions = new Map<Representation.Any, Subscription>()
         const reprCount = new BehaviorSubject(0)
 
@@ -125,7 +131,8 @@ namespace Canvas3D {
         let isUpdating = false
         let drawPending = false
 
-        const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug)
+        const debugHelper = new BoundingSphereHelper(webgl, scene, p.debug);
+        const interactionHelper = new Canvas3dInteractionHelper(identify, getLoci, input);
 
         function getLoci(pickingId: PickingId) {
             let loci: Loci = EmptyLoci
@@ -141,7 +148,8 @@ namespace Canvas3D {
             return { loci, repr }
         }
 
-        function mark(loci: Loci, action: MarkerAction, repr?: Representation.Any) {
+        function mark(reprLoci: Representation.Loci, action: MarkerAction) {
+            const { repr, loci } = reprLoci
             let changed = false
             if (repr) {
                 changed = repr.mark(loci, action)
@@ -149,7 +157,7 @@ namespace Canvas3D {
                 reprRenderObjects.forEach((_, _repr) => { changed = _repr.mark(loci, action) || changed })
             }
             if (changed) {
-                scene.update(true)
+                scene.update(void 0, true)
                 const prevPickDirty = pickDirty
                 draw(true)
                 pickDirty = prevPickDirty // marking does not change picking buffers
@@ -192,7 +200,7 @@ namespace Canvas3D {
             if (isIdentifying || isUpdating) return false
 
             let didRender = false
-            controls.update()
+            controls.update(currentTime);
             // TODO: is this a good fix? Also, setClipping does not work if the user has manually set a clipping plane.
             if (!camera.transition.inTransition) setClipping();
             const cameraChanged = camera.updateMatrices();
@@ -230,6 +238,7 @@ namespace Canvas3D {
         }
 
         let forceNextDraw = false;
+        let currentTime = 0;
 
         function draw(force?: boolean) {
             if (render('draw', !!force || forceNextDraw)) {
@@ -246,9 +255,10 @@ namespace Canvas3D {
         }
 
         function animate() {
-            const t = now();
-            camera.transition.tick(t);
-            draw(false)
+            currentTime = now();
+            camera.transition.tick(currentTime);
+            draw(false);
+            if (!camera.transition.inTransition) interactionHelper.tick(currentTime);
             window.requestAnimationFrame(animate)
         }
 
@@ -300,16 +310,19 @@ namespace Canvas3D {
         function add(repr: Representation.Any) {
             isUpdating = true
             const oldRO = reprRenderObjects.get(repr)
-            const newRO = new Set<RenderObject>()
+            const newRO = new Set<GraphicsRenderObject>()
             repr.renderObjects.forEach(o => newRO.add(o))
+
             if (oldRO) {
-                SetUtils.difference(newRO, oldRO).forEach(o => scene.add(o))
-                SetUtils.difference(oldRO, newRO).forEach(o => scene.remove(o))
+                if (!SetUtils.areEqual(newRO, oldRO)) {
+                    for (const o of Array.from(newRO)) { if (!oldRO.has(o)) scene.add(o); }
+                    for (const o of Array.from(oldRO)) { if (!newRO.has(o)) scene.remove(o) }
+                }
             } else {
                 repr.renderObjects.forEach(o => scene.add(o))
             }
             reprRenderObjects.set(repr, newRO)
-            scene.update()
+            scene.update(repr.renderObjects, false)
             if (debugHelper.isEnabled) debugHelper.update()
             isUpdating = false
             requestDraw(true)
@@ -337,14 +350,21 @@ namespace Canvas3D {
                     isUpdating = true
                     renderObjects.forEach(o => scene.remove(o))
                     reprRenderObjects.delete(repr)
-                    scene.update()
+                    scene.update(void 0, false)
                     if (debugHelper.isEnabled) debugHelper.update()
                     isUpdating = false
                     requestDraw(true)
                     reprCount.next(reprRenderObjects.size)
                 }
             },
-            update: () => scene.update(),
+            update: (repr, keepSphere) => {
+                if (repr) {
+                    if (!reprRenderObjects.has(repr)) return;
+                    scene.update(repr.renderObjects, !!keepSphere);
+                } else {
+                    scene.update(void 0, !!keepSphere)
+                }
+            },
             clear: () => {
                 reprRenderObjects.clear()
                 scene.clear()
@@ -415,6 +435,9 @@ namespace Canvas3D {
             get stats() {
                 return renderer.stats
             },
+            get interaction() {
+                return interactionHelper.events
+            },
             dispose: () => {
                 scene.clear()
                 debugHelper.clear()
@@ -422,6 +445,7 @@ namespace Canvas3D {
                 controls.dispose()
                 renderer.dispose()
                 camera.dispose()
+                interactionHelper.dispose()
             }
         }
 

+ 40 - 21
src/mol-canvas3d/controls/trackball.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 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>
@@ -21,6 +21,9 @@ export const TrackballControlsParams = {
     zoomSpeed: PD.Numeric(6.0, { min: 0.1, max: 10, step: 0.1 }),
     panSpeed: PD.Numeric(0.8, { min: 0.1, max: 5, step: 0.1 }),
 
+    spin: PD.Boolean(false),
+    spinSpeed: PD.Numeric(1, { min: -100, max: 100, step: 1 }),
+
     staticMoving: PD.Boolean(true, { isHidden: true }),
     dynamicDampingFactor: PD.Numeric(0.2, {}, { isHidden: true }),
 
@@ -36,12 +39,12 @@ interface TrackballControls {
     readonly props: Readonly<TrackballControlsProps>
     setProps: (props: Partial<TrackballControlsProps>) => void
 
-    update: () => void
+    update: (t: number) => void
     reset: () => void
     dispose: () => void
 }
 namespace TrackballControls {
-    export function create (input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
+    export function create(input: InputObserver, object: Object3D & { target: Vec3 }, props: Partial<TrackballControlsProps> = {}): TrackballControls {
         const p = { ...PD.getDefaultValues(TrackballControlsParams), ...props }
 
         const viewport: Viewport = { x: 0, y: 0, width: 0, height: 0 }
@@ -50,9 +53,12 @@ namespace TrackballControls {
         let disposed = false
 
         const dragSub = input.drag.subscribe(onDrag)
+        const interactionEndSub = input.interactionEnd.subscribe(onInteractionEnd)
         const wheelSub = input.wheel.subscribe(onWheel)
         const pinchSub = input.pinch.subscribe(onPinch)
 
+        let _isInteracting = false;
+
         // For internal use
         const lastPosition = Vec3.zero()
 
@@ -67,9 +73,6 @@ namespace TrackballControls {
         const _zoomStart = Vec2.zero()
         const _zoomEnd = Vec2.zero()
 
-        let _touchZoomDistanceStart = 0
-        let _touchZoomDistanceEnd = 0
-
         const _panStart = Vec2.zero()
         const _panEnd = Vec2.zero()
 
@@ -125,7 +128,7 @@ namespace TrackballControls {
                 Vec3.normalize(rotAxis, Vec3.cross(rotAxis, rotMoveDir, _eye))
 
                 angle *= p.rotateSpeed;
-                Quat.setAxisAngle(rotQuat, rotAxis, angle )
+                Quat.setAxisAngle(rotQuat, rotAxis, angle)
 
                 Vec3.transformQuat(_eye, _eye, rotQuat)
                 Vec3.transformQuat(object.up, object.up, rotQuat)
@@ -144,7 +147,7 @@ namespace TrackballControls {
             Vec2.copy(_movePrev, _moveCurr)
         }
 
-        function zoomCamera () {
+        function zoomCamera() {
             const factor = 1.0 + (_zoomEnd[1] - _zoomStart[1]) * p.zoomSpeed
             if (factor !== 1.0 && factor > 0.0) {
                 Vec3.scale(_eye, _eye, factor)
@@ -201,8 +204,12 @@ namespace TrackballControls {
             }
         }
 
+        let lastUpdated = -1;
         /** Update the object's position, direction and up vectors */
-        function update() {
+        function update(t: number) {
+            if (lastUpdated === t) return;
+            if (p.spin) spin(t - lastUpdated);
+
             Vec3.sub(_eye, object.position, target)
 
             rotateCamera()
@@ -218,6 +225,8 @@ namespace TrackballControls {
             if (Vec3.squaredDistance(lastPosition, object.position) > EPSILON.Value) {
                 Vec3.copy(lastPosition, object.position)
             }
+
+            lastUpdated = t;
         }
 
         /** Reset object's vectors and the target vector to their initial values */
@@ -233,7 +242,9 @@ namespace TrackballControls {
 
         // listeners
 
-        function onDrag({ pageX, pageY, buttons, modifiers, isStart }: DragInput) {
+        function onDrag({ pageX, pageY, buttons, isStart }: DragInput) {
+            _isInteracting = true;
+
             if (isStart) {
                 if (buttons === ButtonsType.Flag.Primary) {
                     Vec2.copy(_moveCurr, getMouseOnCircle(pageX, pageY))
@@ -257,19 +268,17 @@ namespace TrackballControls {
             }
         }
 
+        function onInteractionEnd() {
+            _isInteracting = false;
+        }
+
         function onWheel({ dy }: WheelInput) {
             _zoomStart[1] -= dy * 0.0001
         }
 
-        function onPinch({ distance, isStart }: PinchInput) {
-            if (isStart) {
-                _touchZoomDistanceStart = distance
-            }
-            _touchZoomDistanceEnd = distance
-
-            const factor = (_touchZoomDistanceStart / _touchZoomDistanceEnd) * p.zoomSpeed
-            _touchZoomDistanceStart = _touchZoomDistanceEnd;
-            Vec3.scale(_eye, _eye, factor)
+        function onPinch({ fraction }: PinchInput) {
+            _isInteracting = true;
+            _zoomStart[1] -= (fraction - 1) * 0.1
         }
 
         function dispose() {
@@ -279,16 +288,26 @@ namespace TrackballControls {
             dragSub.unsubscribe()
             wheelSub.unsubscribe()
             pinchSub.unsubscribe()
+            interactionEndSub.unsubscribe()
+        }
+
+        const _spinSpeed = Vec2.create(0.005, 0);
+        function spin(deltaT: number) {
+            const frameSpeed = (p.spinSpeed || 0) / 1000;
+            _spinSpeed[0] = 60 * Math.min(Math.abs(deltaT), 1000 / 8) / 1000 * frameSpeed;
+            if (!_isInteracting) Vec2.add(_moveCurr, _movePrev, _spinSpeed);
         }
 
         // force an update at start
-        update();
+        update(0);
 
         return {
             viewport,
 
             get props() { return p as Readonly<TrackballControlsProps> },
-            setProps: (props: Partial<TrackballControlsProps>) => { Object.assign(p, props) },
+            setProps: (props: Partial<TrackballControlsProps>) => {
+                Object.assign(p, props)
+            },
 
             update,
             reset,

+ 4 - 2
src/mol-canvas3d/helper/bounding-sphere-helper.ts

@@ -57,7 +57,9 @@ export class BoundingSphereHelper {
                 const instanceData = this.instancesData.get(ro)
                 const newInstanceData = updateBoundingSphereData(this.scene, r.values.invariantBoundingSphere.ref.value, instanceData, ColorNames.skyblue, {
                     aTransform: ro.values.aTransform,
+                    matrix: ro.values.matrix,
                     transform: ro.values.transform,
+                    extraTransform: ro.values.extraTransform,
                     uInstanceCount: ro.values.uInstanceCount,
                     instanceCount: ro.values.instanceCount,
                     aInstance: ro.values.aInstance,
@@ -79,7 +81,7 @@ export class BoundingSphereHelper {
             }
         })
 
-        this.scene.update()
+        this.scene.update(void 0, false);
     }
 
     syncVisibility() {
@@ -136,5 +138,5 @@ function createBoundingSphereMesh(boundingSphere: Sphere3D, mesh?: Mesh) {
 
 function createBoundingSphereRenderObject(mesh: Mesh, color: Color, transform?: TransformData) {
     const values = Mesh.Utils.createValuesSimple(mesh, { alpha: 0.1, doubleSided: false }, color, 1, transform)
-    return createRenderObject('mesh', values, { visible: true, pickable: false, opaque: false })
+    return createRenderObject('mesh', values, { visible: true, alphaFactor: 1, pickable: false, opaque: false })
 }

+ 125 - 0
src/mol-canvas3d/helper/interaction-events.ts

@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ */
+
+import { PickingId } from 'mol-geo/geometry/picking';
+import { EmptyLoci } from 'mol-model/loci';
+import { Representation } from 'mol-repr/representation';
+import InputObserver, { ModifiersKeys, ButtonsType } from 'mol-util/input/input-observer';
+import { RxEventHelper } from 'mol-util/rx-event-helper';
+
+type Canvas3D = import('../canvas3d').Canvas3D
+
+export class Canvas3dInteractionHelper {
+    private ev = RxEventHelper.create();
+
+    readonly events = {
+        highlight: this.ev<import('../canvas3d').Canvas3D.HighlightEvent>(),
+        click: this.ev<import('../canvas3d').Canvas3D.ClickEvent>(),
+    };
+
+    private cX = -1;
+    private cY = -1;
+
+    private lastX = -1;
+    private lastY = -1;
+
+    private id: PickingId | undefined = void 0;
+
+    private currentIdentifyT = 0;
+
+    private prevLoci: Representation.Loci = Representation.Loci.Empty;
+    private prevT = 0;
+
+    private inside = false;
+
+    private buttons: ButtonsType = ButtonsType.create(0);
+    private modifiers: ModifiersKeys = ModifiersKeys.None;
+
+    private async identify(isClick: boolean, t: number) {
+        if (this.lastX !== this.cX && this.lastY !== this.cY) {
+            this.id = await this.canvasIdentify(this.cX, this.cY);
+            this.lastX = this.cX;
+            this.lastY = this.cY;
+        }
+
+        if (!this.id) return;
+
+        if (isClick) {
+            this.events.click.next({ current: this.getLoci(this.id), buttons: this.buttons, modifiers: this.modifiers });
+            return;
+        }
+
+        // only highlight the latest
+        if (!this.inside || this.currentIdentifyT !== t) {
+            return;
+        }
+
+        const loci = this.getLoci(this.id);
+        if (!Representation.Loci.areEqual(this.prevLoci, loci)) {
+            this.events.highlight.next({ current: loci, prev: this.prevLoci, modifiers: this.modifiers });
+            this.prevLoci = loci;
+        }
+    }
+
+    tick(t: number) {
+        if (this.inside && t - this.prevT > 1000 / this.maxFps) {
+            this.prevT = t;
+            this.currentIdentifyT = t;
+            this.identify(false, t);
+        }
+    }
+
+    leave() {
+        this.inside = false;
+        if (this.prevLoci.loci !== EmptyLoci) {
+            const prev = this.prevLoci;
+            this.prevLoci = Representation.Loci.Empty;
+            this.events.highlight.next({ current: this.prevLoci, prev });
+        }
+    }
+
+    move(x: number, y: number, modifiers: ModifiersKeys) {
+        this.inside = true;
+        this.modifiers = modifiers;
+        this.cX = x;
+        this.cY = y;
+    }
+
+    select(x: number, y: number, buttons: ButtonsType, modifiers: ModifiersKeys) {
+        this.cX = x;
+        this.cY = y;
+        this.buttons = buttons;
+        this.modifiers = modifiers;
+        this.identify(true, 0);
+    }
+
+    modify(modifiers: ModifiersKeys) {
+        if (this.prevLoci.loci === EmptyLoci || ModifiersKeys.areEqual(modifiers, this.modifiers)) return;
+        this.modifiers = modifiers;
+        this.events.highlight.next({ current: this.prevLoci, prev: this.prevLoci, modifiers: this.modifiers });
+    }
+
+    dispose() {
+        this.ev.dispose();
+    }
+
+    constructor(private canvasIdentify: Canvas3D['identify'], private getLoci: Canvas3D['getLoci'], input: InputObserver, private maxFps: number = 15) {
+        input.move.subscribe(({x, y, inside, buttons, modifiers }) => {
+            if (!inside || buttons) { return; }
+            this.move(x, y, modifiers);
+        });
+
+        input.leave.subscribe(() => {
+            this.leave();
+        });
+
+        input.click.subscribe(({x, y, buttons, modifiers }) => {
+            this.select(x, y, buttons, modifiers);
+        });
+
+        input.modifiers.subscribe(modifiers => this.modify(modifiers));
+    }
+}

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

@@ -140,6 +140,8 @@ describe('ordered set', () => {
     testEq('union AA3', OrderedSet.union(OrderedSet.ofSortedArray([1, 3]), OrderedSet.ofSortedArray([2, 4])), [1, 2, 3, 4]);
     testEq('union AA4', OrderedSet.union(OrderedSet.ofSortedArray([1, 3]), OrderedSet.ofSortedArray([1, 3, 4])), [1, 3, 4]);
     testEq('union AA5', OrderedSet.union(OrderedSet.ofSortedArray([1, 3, 4]), OrderedSet.ofSortedArray([1, 3])), [1, 3, 4]);
+    testEq('union AR', OrderedSet.union(OrderedSet.ofSortedArray([1, 2, 5, 6]), OrderedSet.ofRange(3, 4)), [1, 2, 3, 4, 5, 6]);
+    testEq('union AR1', OrderedSet.union(OrderedSet.ofSortedArray([1, 2, 6, 7]), OrderedSet.ofRange(3, 4)), [1, 2, 3, 4, 6, 7]);
     it('union AA6', () => expect(OrderedSet.union(arr136, OrderedSet.ofSortedArray([1, 3, 6]))).toBe(arr136));
 
     testEq('intersect ES', OrderedSet.intersect(empty, singleton10), []);
@@ -164,6 +166,7 @@ describe('ordered set', () => {
     testEq('subtract SR2', OrderedSet.subtract(range1_4, OrderedSet.ofSingleton(3)), [1, 2, 4]);
     testEq('subtract RR', OrderedSet.subtract(range1_4, range1_4), []);
     testEq('subtract RR1', OrderedSet.subtract(range1_4, OrderedSet.ofRange(3, 5)), [1, 2]);
+    testEq('subtract RR2', OrderedSet.subtract(range1_4, OrderedSet.ofRange(2, 3)), [1, 4]);
 
     testEq('subtract RA', OrderedSet.subtract(range1_4, arr136), [2, 4]);
     testEq('subtract RA1', OrderedSet.subtract(range1_4, OrderedSet.ofSortedArray([0, 1, 2, 3, 4, 7])), []);

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

@@ -169,7 +169,7 @@ function unionSI(a: S, b: I) {
     let offset = 0;
     for (let i = 0; i < start; i++) indices[offset++] = a[i];
     for (let i = min; i <= max; i++) indices[offset++] = i;
-    for (let i = end, _i = a.length; i < _i; i++) indices[offset] = a[i];
+    for (let i = end, _i = a.length; i < _i; i++) indices[offset++] = a[i];
 
     return ofSortedArray(indices);
 }

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

@@ -32,7 +32,7 @@ export function hashCode(xs: Nums) {
     // hash of tuple (size, min, max, mid)
     const s = xs.length;
     if (!s) return 0;
-    if (s > 2) return hash4(s, xs[0], xs[s - 1], xs[s << 1]);
+    if (s > 2) return hash4(s, xs[0], xs[s - 1], xs[s >> 1]);
     return hash3(s, xs[0], xs[s - 1]);
 }
 

+ 4 - 2
src/mol-geo/geometry/base.ts

@@ -60,6 +60,7 @@ export namespace BaseGeometry {
 
     export function createValues(props: PD.Values<Params>, counts: Counts) {
         return {
+            alpha: ValueCell.create(props.alpha),
             uAlpha: ValueCell.create(props.alpha),
             uHighlightColor: ValueCell.create(Color.toArrayNormalized(props.highlightColor, Vec3.zero(), 0)),
             uSelectColor: ValueCell.create(Color.toArrayNormalized(props.selectColor, Vec3.zero(), 0)),
@@ -76,19 +77,20 @@ export namespace BaseGeometry {
         if (Color.fromNormalizedArray(values.uSelectColor.ref.value, 0) !== props.selectColor) {
             ValueCell.update(values.uSelectColor, Color.toArrayNormalized(props.selectColor, values.uSelectColor.ref.value, 0))
         }
-        ValueCell.updateIfChanged(values.uAlpha, props.alpha)
+        ValueCell.updateIfChanged(values.alpha, props.alpha) // `uAlpha` is set in renderable.render
         ValueCell.updateIfChanged(values.dUseFog, props.useFog)
     }
 
     export function createRenderableState(props: Partial<PD.Values<Params>> = {}): RenderableState {
         return {
             visible: true,
+            alphaFactor: 1,
             pickable: true,
             opaque: props.alpha === undefined ? true : props.alpha === 1
         }
     }
 
     export function updateRenderableState(state: RenderableState, props: PD.Values<Params>) {
-        state.opaque = props.alpha === 1
+        state.opaque = props.alpha * state.alphaFactor >= 1
     }
 }

+ 4 - 4
src/mol-geo/geometry/color-data.ts

@@ -41,7 +41,7 @@ export function createValueColor(value: Color, colorData?: ColorData): ColorData
     } else {
         return {
             uColor: ValueCell.create(Color.toRgbNormalized(value) as Vec3),
-            tColor: ValueCell.create({ disabled: true, array: new Uint8Array(3), width: 1, height: 1 }),
+            tColor: ValueCell.create({ array: new Uint8Array(3), width: 1, height: 1 }),
             uColorTexDim: ValueCell.create(Vec2.create(1, 1)),
             dColorType: ValueCell.create('uniform'),
         }
@@ -74,7 +74,7 @@ export function createTextureColor(colors: TextureImage<Uint8Array>, type: Color
 /** Creates color texture with color for each instance/unit */
 export function createInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { instanceCount } = locationIt
-    const colors = colorData && colorData.tColor.ref.value.array.length >= instanceCount * 3 ? colorData.tColor.ref.value : createTextureImage(instanceCount, 3)
+    const colors = createTextureImage(Math.max(1, instanceCount), 3, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext) {
         const { location, isSecondary, instanceIndex } = locationIt.move()
@@ -87,7 +87,7 @@ export function createInstanceColor(locationIt: LocationIterator, color: Locatio
 /** Creates color texture with color for each group (i.e. shared across instances/units) */
 export function createGroupColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount } = locationIt
-    const colors = colorData && colorData.tColor.ref.value.array.length >= groupCount * 3 ? colorData.tColor.ref.value : createTextureImage(groupCount, 3)
+    const colors = createTextureImage(Math.max(1, groupCount), 3, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const { location, isSecondary, groupIndex } = locationIt.move()
@@ -100,7 +100,7 @@ export function createGroupColor(locationIt: LocationIterator, color: LocationCo
 export function createGroupInstanceColor(locationIt: LocationIterator, color: LocationColor, colorData?: ColorData): ColorData {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
-    const colors = colorData && colorData.tColor.ref.value.array.length >= count * 3 ? colorData.tColor.ref.value : createTextureImage(count, 3)
+    const colors = createTextureImage(Math.max(1, count), 3, colorData && colorData.tColor.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext) {
         const { location, isSecondary, index } = locationIt.move()

+ 6 - 3
src/mol-geo/geometry/direct-volume/direct-volume.ts

@@ -24,6 +24,7 @@ import { RenderableState } from 'mol-gl/renderable';
 import { ColorListOptions, ColorListName } from 'mol-util/color/scale';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 const VolumeBox = Box()
 const RenderModeOptions = [['isosurface', 'Isosurface'], ['volume', 'Volume']] as [string, string][]
@@ -73,7 +74,7 @@ export namespace DirectVolume {
 
     export const Params = {
         ...BaseGeometry.Params,
-        isoValue: PD.Numeric(0.22, { min: -1, max: 1, step: 0.01 }),
+        isoValueNorm: PD.Numeric(0.22, { min: 0, max: 1, step: 0.01 }, { description: 'Normalized Isolevel Value' }),
         renderMode: PD.Select('volume', RenderModeOptions),
         controlPoints: PD.LineGraph([
             Vec2.create(0.19, 0.0), Vec2.create(0.2, 0.05), Vec2.create(0.25, 0.05), Vec2.create(0.26, 0.0),
@@ -101,6 +102,7 @@ export namespace DirectVolume {
         const { instanceCount, groupCount } = locationIt
         const color = createColors(locationIt, theme.color)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: VolumeBox.indices.length, groupCount, instanceCount }
 
@@ -114,6 +116,7 @@ export namespace DirectVolume {
         return {
             ...color,
             ...marker,
+            ...overpaint,
             ...transform,
             ...BaseGeometry.createValues(props, counts),
 
@@ -122,7 +125,7 @@ export namespace DirectVolume {
             boundingSphere: ValueCell.create(boundingSphere),
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
 
-            uIsoValue: ValueCell.create(props.isoValue),
+            uIsoValue: ValueCell.create(props.isoValueNorm),
             uBboxMin: bboxMin,
             uBboxMax: bboxMax,
             uBboxSize: bboxSize,
@@ -145,7 +148,7 @@ export namespace DirectVolume {
     }
 
     function updateValues(values: DirectVolumeValues, props: PD.Values<Params>) {
-        ValueCell.updateIfChanged(values.uIsoValue, props.isoValue)
+        ValueCell.updateIfChanged(values.uIsoValue, props.isoValueNorm)
         ValueCell.updateIfChanged(values.uAlpha, props.alpha)
         ValueCell.updateIfChanged(values.dUseFog, props.useFog)
         ValueCell.updateIfChanged(values.dRenderMode, props.renderMode)

+ 30 - 12
src/mol-geo/geometry/lines/lines-builder.ts

@@ -7,12 +7,18 @@
 import { ValueCell } from 'mol-util/value-cell'
 import { ChunkedArray } from 'mol-data/util';
 import { Lines } from './lines';
+import { Mat4, Vec3 } from 'mol-math/linear-algebra';
+import { Cage } from 'mol-geo/primitive/cage';
 
 export interface LinesBuilder {
     add(startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number): void
+    addCage(t: Mat4, cage: Cage, group: number): void
     getLines(): Lines
 }
 
+const tmpVecA = Vec3.zero()
+const tmpVecB = Vec3.zero()
+
 export namespace LinesBuilder {
     export function create(initialCount = 2048, chunkSize = 1024, lines?: Lines): LinesBuilder {
         const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, lines ? lines.mappingBuffer.ref.value : initialCount);
@@ -21,20 +27,32 @@ export namespace LinesBuilder {
         const starts = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.startBuffer.ref.value : initialCount);
         const ends = ChunkedArray.create(Float32Array, 3, chunkSize, lines ? lines.endBuffer.ref.value : initialCount);
 
+        const add = (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number) => {
+            const offset = mappings.elementCount
+            for (let i = 0; i < 4; ++i) {
+                ChunkedArray.add3(starts, startX, startY, startZ);
+                ChunkedArray.add3(ends, endX, endY, endZ);
+                ChunkedArray.add(groups, group);
+            }
+            ChunkedArray.add2(mappings, -1, 1);
+            ChunkedArray.add2(mappings, -1, -1);
+            ChunkedArray.add2(mappings, 1, 1);
+            ChunkedArray.add2(mappings, 1, -1);
+            ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
+            ChunkedArray.add3(indices, offset + 1, offset + 3, offset + 2);
+        }
+
         return {
-            add: (startX: number, startY: number, startZ: number, endX: number, endY: number, endZ: number, group: number) => {
-                const offset = mappings.elementCount
-                for (let i = 0; i < 4; ++i) {
-                    ChunkedArray.add3(starts, startX, startY, startZ);
-                    ChunkedArray.add3(ends, endX, endY, endZ);
-                    ChunkedArray.add(groups, group);
+            add,
+            addCage: (t: Mat4, cage: Cage, group: number) => {
+                const { vertices, edges } = cage
+                for (let i = 0, il = edges.length; i < il; i += 2) {
+                    Vec3.fromArray(tmpVecA, vertices, edges[i] * 3)
+                    Vec3.fromArray(tmpVecB, vertices, edges[i + 1] * 3)
+                    Vec3.transformMat4(tmpVecA, tmpVecA, t)
+                    Vec3.transformMat4(tmpVecB, tmpVecB, t)
+                    add(tmpVecA[0], tmpVecA[1], tmpVecA[2], tmpVecB[0], tmpVecB[1], tmpVecB[2], group)
                 }
-                ChunkedArray.add2(mappings, -1, 1);
-                ChunkedArray.add2(mappings, -1, -1);
-                ChunkedArray.add2(mappings, 1, 1);
-                ChunkedArray.add2(mappings, 1, -1);
-                ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
-                ChunkedArray.add3(indices, offset + 1, offset + 3, offset + 2);
             },
             getLines: () => {
                 const mb = ChunkedArray.compact(mappings, true) as Float32Array

+ 5 - 2
src/mol-geo/geometry/lines/lines.ts

@@ -22,6 +22,7 @@ import { Sphere3D } from 'mol-math/geometry';
 import { Theme } from 'mol-theme/theme';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 /** Wide line */
 export interface Lines {
@@ -117,6 +118,7 @@ export namespace Lines {
         const color = createColors(locationIt, theme.color)
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: lines.lineCount * 2 * 3, groupCount, instanceCount }
 
@@ -134,6 +136,7 @@ export namespace Lines {
             ...color,
             ...size,
             ...marker,
+            ...overpaint,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),
@@ -171,8 +174,8 @@ export namespace Lines {
 }
 
 function getBoundingSphere(lineStart: Float32Array, lineEnd: Float32Array, lineCount: number, transform: Float32Array, transformCount: number) {
-    const start = calculateBoundingSphere(lineStart, lineCount, transform, transformCount)
-    const end = calculateBoundingSphere(lineEnd, lineCount, transform, transformCount)
+    const start = calculateBoundingSphere(lineStart, lineCount * 4, transform, transformCount)
+    const end = calculateBoundingSphere(lineEnd, lineCount * 4, transform, transformCount)
     return {
         boundingSphere: Sphere3D.addSphere(start.boundingSphere, end.boundingSphere),
         invariantBoundingSphere: Sphere3D.addSphere(start.invariantBoundingSphere, end.invariantBoundingSphere)

+ 7 - 7
src/mol-geo/geometry/marker-data.ts

@@ -38,12 +38,14 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
                 }
                 break
             case MarkerAction.Select:
-                v += 2
+                if (v < 2) v += 2
+                // v += 2
                 break
             case MarkerAction.Deselect:
-                if (v >= 2) {
-                    v -= 2
-                }
+                // if (v >= 2) {
+                //     v -= 2
+                // }
+                v = v % 2
                 break
             case MarkerAction.Toggle:
                 if (v >= 2) {
@@ -63,9 +65,7 @@ export function applyMarkerAction(array: Uint8Array, start: number, end: number,
 }
 
 export function createMarkers(count: number, markerData?: MarkerData): MarkerData {
-    const markers = markerData && markerData.tMarker.ref.value.array.length >= count
-        ? markerData.tMarker.ref.value
-        : createTextureImage(count, 1)
+    const markers = createTextureImage(Math.max(1, count), 1, markerData && markerData.tMarker.ref.value.array)
     if (markerData) {
         ValueCell.update(markerData.tMarker, markers)
         ValueCell.update(markerData.uMarkerTexDim, Vec2.create(markers.width, markers.height))

+ 15 - 6
src/mol-geo/geometry/mesh/builder/sheet.ts

@@ -47,12 +47,21 @@ function addCap(offset: number, state: MeshBuilder.State, controlPoints: ArrayLi
     Vec3.sub(p3, Vec3.sub(p3, positionVector, horizontalVector), verticalLeftVector)
     Vec3.add(p4, Vec3.sub(p4, positionVector, horizontalVector), verticalRightVector)
 
-    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])
+    if (leftHeight < rightHeight) {
+        ChunkedArray.add3(vertices, p4[0], p4[1], p4[2])
+        ChunkedArray.add3(vertices, p3[0], p3[1], p3[2])
+        ChunkedArray.add3(vertices, p2[0], p2[1], p2[2])
+        ChunkedArray.add3(vertices, p1[0], p1[1], p1[2])
+        Vec3.copy(verticalVector, verticalRightVector)
+    } else {
+        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.copy(verticalVector, verticalLeftVector)
+    }
 
-    Vec3.cross(normalVector, horizontalVector, verticalLeftVector)
+    Vec3.cross(normalVector, horizontalVector, verticalVector)
 
     for (let i = 0; i < 4; ++i) {
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2])
@@ -152,8 +161,8 @@ export function addSheet(state: MeshBuilder.State, controlPoints: ArrayLike<numb
         const h = arrowHeight === 0 ? height : arrowHeight
         addCap(0, state, controlPoints, normalVectors, binormalVectors, width, h, h)
     } else if (arrowHeight > 0) {
-        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
         addCap(0, state, controlPoints, normalVectors, binormalVectors, width, arrowHeight, -height)
+        addCap(0, state, controlPoints, normalVectors, binormalVectors, width, -arrowHeight, height)
     }
 
     if (endCap && arrowHeight === 0) {

+ 19 - 0
src/mol-geo/geometry/mesh/builder/triangle.ts

@@ -0,0 +1,19 @@
+/**
+ * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import {  Mat4 } from 'mol-math/linear-algebra';
+import { MeshBuilder } from '../mesh-builder';
+
+const tmpSphereMat = Mat4.identity()
+
+function getTriangle(vertices: number[], normals: number[], indices: number[]) {
+
+    return {vertices, normals, indices};
+}
+
+export function addTriangle(state: MeshBuilder.State, triangle_vertices: number[], triangle_normals: number[], triangle_indices: number[]) {
+    MeshBuilder.addPrimitive(state, tmpSphereMat, getTriangle( triangle_vertices, triangle_normals, triangle_indices))
+}

+ 10 - 1
src/mol-geo/geometry/mesh/builder/tube.ts

@@ -27,7 +27,7 @@ function add3AndScale2(out: Vec3, a: Vec3, b: Vec3, c: Vec3, sa: number, sb: num
     out[2] = (a[2] * sa) + (b[2] * sb) + c[2];
 }
 
-export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, width: number, height: number, waveFactor: number, startCap: boolean, endCap: boolean) {
+export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<number>, normalVectors: ArrayLike<number>, binormalVectors: ArrayLike<number>, linearSegments: number, radialSegments: number, widthValues: ArrayLike<number>, heightValues: ArrayLike<number>, waveFactor: number, startCap: boolean, endCap: boolean) {
     const { currentGroup, vertices, normals, indices, groups } = state
 
     let vertexCount = vertices.elementCount
@@ -39,6 +39,9 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         Vec3.fromArray(v, binormalVectors, i3)
         Vec3.fromArray(controlPoint, controlPoints, i3)
 
+        const width = widthValues[i]
+        const height = heightValues[i]
+
         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;
@@ -83,6 +86,9 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
+        const width = widthValues[0]
+        const height = heightValues[0]
+
         vertexCount = vertices.elementCount
         for (let i = 0; i < radialSegments; ++i) {
             const t = 2 * Math.PI * i / radialSegments;
@@ -112,6 +118,9 @@ export function addTube(state: MeshBuilder.State, controlPoints: ArrayLike<numbe
         ChunkedArray.add3(vertices, controlPoint[0], controlPoint[1], controlPoint[2]);
         ChunkedArray.add3(normals, normalVector[0], normalVector[1], normalVector[2]);
 
+        const width = widthValues[linearSegments]
+        const height = heightValues[linearSegments]
+
         vertexCount = vertices.elementCount
         for (let i = 0; i < radialSegments; ++i) {
             const t = 2 * Math.PI * i / radialSegments

+ 61 - 1
src/mol-geo/geometry/mesh/mesh-builder.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -10,9 +10,16 @@ import { ChunkedArray } from 'mol-data/util';
 import { Mesh } from './mesh';
 import { getNormalMatrix } from '../../util';
 import { Primitive } from '../../primitive/primitive';
+import { Cage } from 'mol-geo/primitive/cage';
+import { addSphere } from './builder/sphere';
+import { addCylinder } from './builder/cylinder';
 
 const tmpV = Vec3.zero()
 const tmpMat3 = Mat3.zero()
+const tmpVecA = Vec3.zero()
+const tmpVecB = Vec3.zero()
+const tmpVecC = Vec3.zero()
+const tmpVecD = Vec3.zero()
 
 export namespace MeshBuilder {
     export interface State {
@@ -35,6 +42,45 @@ export namespace MeshBuilder {
         }
     }
 
+    export function addTriangle(state: State, a: Vec3, b: Vec3, c: Vec3) {
+        const { vertices, normals, indices, groups, currentGroup } = state
+        const offset = vertices.elementCount
+        
+        // positions
+        ChunkedArray.add3(vertices, a[0], a[1], a[2]);
+        ChunkedArray.add3(vertices, b[0], b[1], b[2]);
+        ChunkedArray.add3(vertices, c[0], c[1], c[2]);
+
+        Vec3.triangleNormal(tmpV, a, b, c)
+        for (let i = 0; i < 3; ++i) {
+            ChunkedArray.add3(normals, tmpV[0], tmpV[1], tmpV[2]);  // normal
+            ChunkedArray.add(groups, currentGroup);  // group
+        }
+        ChunkedArray.add3(indices, offset, offset + 1, offset + 2);
+    }
+
+    export function addTriangleStrip(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
+        Vec3.fromArray(tmpVecC, vertices, indices[0] * 3)
+        Vec3.fromArray(tmpVecD, vertices, indices[1] * 3)
+        for (let i = 2, il = indices.length; i < il; i += 2) {
+            Vec3.copy(tmpVecA, tmpVecC)
+            Vec3.copy(tmpVecB, tmpVecD)
+            Vec3.fromArray(tmpVecC, vertices, indices[i] * 3)
+            Vec3.fromArray(tmpVecD, vertices, indices[i + 1] * 3)
+            addTriangle(state, tmpVecA, tmpVecB, tmpVecC)
+            addTriangle(state, tmpVecB, tmpVecD, tmpVecC)
+        }
+    }
+
+    export function addTriangleFan(state: State, vertices: ArrayLike<number>, indices: ArrayLike<number>) {
+        Vec3.fromArray(tmpVecA, vertices, indices[0] * 3)
+        for (let i = 2, il = indices.length; i < il; ++i) {
+            Vec3.fromArray(tmpVecB, vertices, indices[i - 1] * 3)
+            Vec3.fromArray(tmpVecC, vertices, indices[i] * 3)
+            addTriangle(state, tmpVecA, tmpVecC, tmpVecB)
+        }
+    }
+
     export function addPrimitive(state: State, t: Mat4, primitive: Primitive) {
         const { vertices: va, normals: na, indices: ia } = primitive
         const { vertices, normals, indices, groups, currentGroup } = state
@@ -55,6 +101,20 @@ export namespace MeshBuilder {
         }
     }
 
+    export function addCage(state: State, t: Mat4, cage: Cage, radius: number, detail: number) {
+        const { vertices: va, edges: ea } = cage
+        const cylinderProps = { radiusTop: radius, radiusBottom: radius }
+        for (let i = 0, il = ea.length; i < il; i += 2) {
+            Vec3.fromArray(tmpVecA, va, ea[i] * 3)
+            Vec3.fromArray(tmpVecB, va, ea[i + 1] * 3)
+            Vec3.transformMat4(tmpVecA, tmpVecA, t)
+            Vec3.transformMat4(tmpVecB, tmpVecB, t)
+            addSphere(state, tmpVecA, radius, detail)
+            addSphere(state, tmpVecB, radius, detail)
+            addCylinder(state, tmpVecA, tmpVecB, 1, cylinderProps)
+        }
+    }
+
     export function getMesh (state: State): Mesh {
         const { vertices, normals, indices, groups, mesh } = state
         const vb = ChunkedArray.compact(vertices, true) as Float32Array

+ 3 - 0
src/mol-geo/geometry/mesh/mesh.ts

@@ -21,6 +21,7 @@ import { Theme } from 'mol-theme/theme';
 import { MeshValues } from 'mol-gl/renderable/mesh';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 export interface Mesh {
     readonly kind: 'mesh',
@@ -381,6 +382,7 @@ export namespace Mesh {
 
         const color = createColors(locationIt, theme.color)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: mesh.triangleCount * 3, groupCount, instanceCount }
 
@@ -398,6 +400,7 @@ export namespace Mesh {
             invariantBoundingSphere: ValueCell.create(invariantBoundingSphere),
             ...color,
             ...marker,
+            ...overpaint,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),

+ 59 - 0
src/mol-geo/geometry/overpaint-data.ts

@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2019 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 { Vec2 } from 'mol-math/linear-algebra'
+import { TextureImage, createTextureImage } from 'mol-gl/renderable/util';
+import { Color } from 'mol-util/color';
+
+export type OverpaintData = {
+    tOverpaint: ValueCell<TextureImage<Uint8Array>>
+    uOverpaintTexDim: ValueCell<Vec2>
+    dOverpaint: ValueCell<boolean>,
+}
+
+export function applyOverpaintColor(array: Uint8Array, start: number, end: number, color: Color, alpha: number) {
+    for (let i = start; i < end; ++i) {
+        Color.toArray(color, array, i * 4)
+        array[i * 4 + 3] = alpha * 255
+    }
+    return true
+}
+
+export function clearOverpaint(array: Uint8Array, start: number, end: number) {
+    array.fill(0, start, end)
+}
+
+export function createOverpaint(count: number, overpaintData?: OverpaintData): OverpaintData {
+    const overpaint = createTextureImage(Math.max(1, count), 4, overpaintData && overpaintData.tOverpaint.ref.value.array)
+    if (overpaintData) {
+        ValueCell.update(overpaintData.tOverpaint, overpaint)
+        ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(overpaint.width, overpaint.height))
+        ValueCell.update(overpaintData.dOverpaint, count > 0)
+        return overpaintData
+    } else {
+        return {
+            tOverpaint: ValueCell.create(overpaint),
+            uOverpaintTexDim: ValueCell.create(Vec2.create(overpaint.width, overpaint.height)),
+            dOverpaint: ValueCell.create(count > 0),
+        }
+    }
+}
+
+const emptyOverpaintTexture = { array: new Uint8Array(4), width: 1, height: 1 }
+export function createEmptyOverpaint(overpaintData?: OverpaintData): OverpaintData {
+    if (overpaintData) {
+        ValueCell.update(overpaintData.tOverpaint, emptyOverpaintTexture)
+        ValueCell.update(overpaintData.uOverpaintTexDim, Vec2.create(1, 1))
+        return overpaintData
+    } else {
+        return {
+            tOverpaint: ValueCell.create(emptyOverpaintTexture),
+            uOverpaintTexDim: ValueCell.create(Vec2.create(1, 1)),
+            dOverpaint: ValueCell.create(false),
+        }
+    }
+}

+ 3 - 0
src/mol-geo/geometry/points/points.ts

@@ -21,6 +21,7 @@ import { PointsValues } from 'mol-gl/renderable/points';
 import { RenderableState } from 'mol-gl/renderable';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 /** Point cloud */
 export interface Points {
@@ -82,6 +83,7 @@ export namespace Points {
         const color = createColors(locationIt, theme.color)
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: points.pointCount, groupCount, instanceCount }
 
@@ -98,6 +100,7 @@ export namespace Points {
             ...color,
             ...size,
             ...marker,
+            ...overpaint,
             ...transform,
 
             ...BaseGeometry.createValues(props, counts),

+ 3 - 3
src/mol-geo/geometry/size-data.ts

@@ -101,7 +101,7 @@ export function createTextureSize(sizes: TextureImage<Uint8Array>, type: SizeTyp
 /** Creates size texture with size for each instance/unit */
 export function createInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { instanceCount} = locationIt
-    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= instanceCount ? sizeData.tSize.ref.value : createTextureImage(instanceCount, 1)
+    const sizes = createTextureImage(Math.max(1, instanceCount), 1, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
@@ -114,7 +114,7 @@ export function createInstanceSize(locationIt: LocationIterator, sizeFn: Locatio
 /** Creates size texture with size for each group (i.e. shared across instances/units) */
 export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { groupCount } = locationIt
-    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= groupCount ? sizeData.tSize.ref.value : createTextureImage(groupCount, 1)
+    const sizes = createTextureImage(Math.max(1, groupCount), 1, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()
@@ -127,7 +127,7 @@ export function createGroupSize(locationIt: LocationIterator, sizeFn: LocationSi
 export function createGroupInstanceSize(locationIt: LocationIterator, sizeFn: LocationSize, sizeData?: SizeData): SizeData {
     const { groupCount, instanceCount } = locationIt
     const count = instanceCount * groupCount
-    const sizes = sizeData && sizeData.tSize.ref.value.array.length >= count ? sizeData.tSize.ref.value : createTextureImage(count, 1)
+    const sizes = createTextureImage(Math.max(1, count), 1, sizeData && sizeData.tSize.ref.value.array)
     locationIt.reset()
     while (locationIt.hasNext && !locationIt.isNextNewInstance) {
         const v = locationIt.move()

+ 1 - 1
src/mol-geo/geometry/spheres/spheres-builder.ts

@@ -54,7 +54,7 @@ export namespace SpheresBuilder {
                     kind: 'spheres',
                     sphereCount: centers.elementCount / 4,
                     centerBuffer: spheres ? ValueCell.update(spheres.centerBuffer, cb) : ValueCell.create(cb),
-                    mappingBuffer: spheres ? ValueCell.update(spheres.centerBuffer, mb) : ValueCell.create(mb),
+                    mappingBuffer: spheres ? ValueCell.update(spheres.mappingBuffer, mb) : ValueCell.create(mb),
                     indexBuffer: spheres ? ValueCell.update(spheres.indexBuffer, ib) : ValueCell.create(ib),
                     groupBuffer: spheres ? ValueCell.update(spheres.groupBuffer, gb) : ValueCell.create(gb),
                 }

+ 3 - 0
src/mol-geo/geometry/spheres/spheres.ts

@@ -18,6 +18,7 @@ import { Sphere3D } from 'mol-math/geometry';
 import { createSizes, getMaxSize } from '../size-data';
 import { Color } from 'mol-util/color';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 /** Spheres */
 export interface Spheres {
@@ -79,6 +80,7 @@ export namespace Spheres {
         const color = createColors(locationIt, theme.color)
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: spheres.sphereCount * 2 * 3, groupCount, instanceCount }
 
@@ -98,6 +100,7 @@ export namespace Spheres {
             ...color,
             ...size,
             ...marker,
+            ...overpaint,
             ...transform,
 
             padding: ValueCell.create(padding),

+ 183 - 21
src/mol-geo/geometry/text/text-builder.ts

@@ -16,7 +16,7 @@ const quadIndices = new Uint16Array([
 ])
 
 export interface TextBuilder {
-    add(str: string, x: number, y: number, z: number, group: number): void
+    add(str: string, x: number, y: number, z: number, depth: number, group: number): void
     getText(): Text
 }
 
@@ -26,65 +26,228 @@ export namespace TextBuilder {
         chunkSize *= 2
         const centers = ChunkedArray.create(Float32Array, 3, chunkSize, text ? text.centerBuffer.ref.value : initialCount);
         const mappings = ChunkedArray.create(Float32Array, 2, chunkSize, text ? text.mappingBuffer.ref.value : initialCount);
+        const depths = ChunkedArray.create(Float32Array, 1, chunkSize, text ? text.depthBuffer.ref.value : initialCount);
         const indices = ChunkedArray.create(Uint32Array, 3, chunkSize, text ? text.indexBuffer.ref.value : initialCount);
         const groups = ChunkedArray.create(Float32Array, 1, chunkSize, text ? text.groupBuffer.ref.value : initialCount);
         const tcoords = ChunkedArray.create(Float32Array, 2, chunkSize, text ? text.tcoordBuffer.ref.value : initialCount);
 
         const p = { ...PD.getDefaultValues(Text.Params), ...props }
-        const { attachment, background, backgroundMargin } = p
+        const { attachment, background, backgroundMargin, tether, tetherLength, tetherBaseWidth } = p
 
         const fontAtlas = getFontAtlas(p)
         const margin = (1 / 2.5) * backgroundMargin
         const outline = fontAtlas.buffer / fontAtlas.lineHeight
 
+        const add = (x: number, y: number, z: number, depth: number, group: number) => {
+            ChunkedArray.add3(centers, x, y, z);
+            ChunkedArray.add(depths, depth);
+            ChunkedArray.add(groups, group);
+        }
+
         return {
-            add: (str: string, x: number, y: number, z: number, group: number) => {
-                let xadvance = 0
+            add: (str: string, x: number, y: number, z: number, depth: number, group: number) => {
+                let bWidth = 0
                 const nChar = str.length
 
                 // calculate width
                 for (let iChar = 0; iChar < nChar; ++iChar) {
                     const c = fontAtlas.get(str[iChar])
-                    xadvance += c.nw - 2 * outline
+                    bWidth += c.nw - 2 * outline
                 }
 
+                const bHeight = 1 / 1.25
+
                 // attachment
                 let yShift: number, xShift: number
+                // vertical
                 if (attachment.startsWith('top')) {
-                    yShift = 1 / 1.25
+                    yShift = bHeight
                 } else if (attachment.startsWith('middle')) {
-                    yShift = 1 / 2.5
+                    yShift = bHeight / 2
                 } else {
                     yShift = 0  // "bottom"
                 }
+                // horizontal
                 if (attachment.endsWith('right')) {
-                    xShift = xadvance
+                    xShift = bWidth
                 } else if (attachment.endsWith('center')) {
-                    xShift = xadvance / 2
+                    xShift = bWidth / 2
                 } else {
                     xShift = 0  // "left"
                 }
 
+                if (tether) {
+                    switch (attachment) {
+                        case 'bottom-left':
+                            xShift -= tetherLength / 2 + margin + 0.1
+                            yShift -= tetherLength / 2 + margin
+                            break
+                        case 'bottom-center':
+                            yShift -= tetherLength + margin
+                            break
+                        case 'bottom-right':
+                            xShift += tetherLength / 2 + margin + 0.1
+                            yShift -= tetherLength / 2 + margin
+                            break
+                        case 'middle-left':
+                            xShift -= tetherLength + margin + 0.1
+                            break
+                        case 'middle-center':
+                            break
+                        case 'middle-right':
+                            xShift += tetherLength + margin + 0.1
+                            break
+                        case 'top-left':
+                            xShift -= tetherLength / 2 + margin + 0.1
+                            yShift += tetherLength / 2 + margin
+                            break
+                        case 'top-center':
+                            yShift += tetherLength + margin
+                            break
+                        case 'top-right':
+                            xShift += tetherLength / 2 + margin + 0.1
+                            yShift += tetherLength / 2 + margin
+                            break
+                    }
+                }
+
+                const xLeft = -xShift - margin - 0.1
+                const xRight = bWidth - xShift + margin + 0.1
+                const yTop = bHeight - yShift + margin
+                const yBottom = -yShift - margin
+
                 // background
                 if (background) {
-                    ChunkedArray.add2(mappings, -xadvance + xShift - margin - 0.1, yShift + margin) // top left
-                    ChunkedArray.add2(mappings, -xadvance + xShift - margin - 0.1, -yShift - margin) // bottom left
-                    ChunkedArray.add2(mappings, xadvance - xShift + margin + 0.1, yShift + margin) // top right
-                    ChunkedArray.add2(mappings, xadvance - xShift + margin + 0.1, -yShift - margin) // bottom right
+                    ChunkedArray.add2(mappings, xLeft, yTop) // top left
+                    ChunkedArray.add2(mappings, xLeft, yBottom) // bottom left
+                    ChunkedArray.add2(mappings, xRight, yTop) // top right
+                    ChunkedArray.add2(mappings, xRight, yBottom) // bottom right
 
                     const offset = centers.elementCount
                     for (let i = 0; i < 4; ++i) {
                         ChunkedArray.add2(tcoords, 10, 10)
-                        ChunkedArray.add3(centers, x, y, z);
-                        ChunkedArray.add(groups, group);
+                        add(x, y, z, depth, group)
                     }
                     ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2])
                     ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5])
                 }
 
+                if (tether) {
+                    let xTip: number, yTip: number
+                    let xBaseA: number, yBaseA: number
+                    let xBaseB: number, yBaseB: number
+                    let xBaseCenter: number, yBaseCenter: number
+                    switch (attachment) {
+                        case 'bottom-left':
+                            xTip = xLeft - tetherLength / 2
+                            xBaseA = xLeft + tetherBaseWidth / 2
+                            xBaseB = xLeft
+                            xBaseCenter = xLeft
+                            yTip = yBottom - tetherLength / 2
+                            yBaseA = yBottom
+                            yBaseB = yBottom + tetherBaseWidth / 2
+                            yBaseCenter = yBottom
+                            break
+                        case 'bottom-center':
+                            xTip = 0
+                            xBaseA = tetherBaseWidth / 2
+                            xBaseB = -tetherBaseWidth / 2
+                            xBaseCenter = 0
+                            yTip = yBottom - tetherLength
+                            yBaseA = yBottom
+                            yBaseB = yBottom
+                            yBaseCenter = yBottom
+                            break
+                        case 'bottom-right':
+                            xTip = xRight + tetherLength / 2
+                            xBaseA = xRight
+                            xBaseB = xRight - tetherBaseWidth / 2
+                            xBaseCenter = xRight
+                            yTip = yBottom - tetherLength / 2
+                            yBaseA = yBottom + tetherBaseWidth / 2
+                            yBaseB = yBottom
+                            yBaseCenter = yBottom
+                            break
+                        case 'middle-left':
+                            xTip = xLeft - tetherLength
+                            xBaseA = xLeft
+                            xBaseB = xLeft
+                            xBaseCenter = xLeft
+                            yTip = 0
+                            yBaseA = -tetherBaseWidth / 2
+                            yBaseB = tetherBaseWidth / 2
+                            yBaseCenter = 0
+                            break
+                        case 'middle-center':
+                            xTip = 0
+                            xBaseA = 0
+                            xBaseB = 0
+                            xBaseCenter = 0
+                            yTip = 0
+                            yBaseA = 0
+                            yBaseB = 0
+                            yBaseCenter = 0
+                            break
+                        case 'middle-right':
+                            xTip = xRight + tetherLength
+                            xBaseA = xRight
+                            xBaseB = xRight
+                            xBaseCenter = xRight
+                            yTip = 0
+                            yBaseA = tetherBaseWidth / 2
+                            yBaseB = -tetherBaseWidth / 2
+                            yBaseCenter = 0
+                            break
+                        case 'top-left':
+                            xTip = xLeft - tetherLength / 2
+                            xBaseA = xLeft + tetherBaseWidth / 2
+                            xBaseB = xLeft
+                            xBaseCenter = xLeft
+                            yTip = yTop + tetherLength / 2
+                            yBaseA = yTop
+                            yBaseB = yTop - tetherBaseWidth / 2
+                            yBaseCenter = yTop
+                            break
+                        case 'top-center':
+                            xTip = 0
+                            xBaseA = tetherBaseWidth / 2
+                            xBaseB = -tetherBaseWidth / 2
+                            xBaseCenter = 0
+                            yTip = yTop + tetherLength
+                            yBaseA = yTop
+                            yBaseB = yTop
+                            yBaseCenter = yTop
+                            break
+                        case 'top-right':
+                            xTip = xRight + tetherLength / 2
+                            xBaseA = xRight
+                            xBaseB = xRight - tetherBaseWidth / 2
+                            xBaseCenter = xRight
+                            yTip = yTop + tetherLength / 2
+                            yBaseA = yTop - tetherBaseWidth / 2
+                            yBaseB = yTop
+                            yBaseCenter = yTop
+                            break
+                        default:
+                            throw new Error('unsupported attachment')
+                    }
+                    ChunkedArray.add2(mappings, xTip, yTip) // tip
+                    ChunkedArray.add2(mappings, xBaseA, yBaseA) // base A
+                    ChunkedArray.add2(mappings, xBaseB, yBaseB) // base B
+                    ChunkedArray.add2(mappings, xBaseCenter, yBaseCenter) // base center
+
+                    const offset = centers.elementCount
+                    for (let i = 0; i < 4; ++i) {
+                        ChunkedArray.add2(tcoords, 10, 10)
+                        add(x, y, z, depth, group)
+                    }
+                    ChunkedArray.add3(indices, offset, offset + 1, offset + 3)
+                    ChunkedArray.add3(indices, offset, offset + 3, offset + 2)
+                }
+
                 xShift += outline
                 yShift += outline
-                xadvance = 0
+                let xadvance = 0
 
                 for (let iChar = 0; iChar < nChar; ++iChar) {
                     const c = fontAtlas.get(str[iChar])
@@ -105,27 +268,26 @@ export namespace TextBuilder {
                     xadvance += c.nw - 2 * outline
 
                     const offset = centers.elementCount
-                    for (let i = 0; i < 4; ++i) {
-                        ChunkedArray.add3(centers, x, y, z);
-                        ChunkedArray.add(groups, group);
-                    }
+                    for (let i = 0; i < 4; ++i) add(x, y, z, depth, group)
                     ChunkedArray.add3(indices, offset + quadIndices[0], offset + quadIndices[1], offset + quadIndices[2])
                     ChunkedArray.add3(indices, offset + quadIndices[3], offset + quadIndices[4], offset + quadIndices[5])
                 }
             },
             getText: () => {
+                const ft = fontAtlas.texture
                 const cb = ChunkedArray.compact(centers, true) as Float32Array
                 const mb = ChunkedArray.compact(mappings, true) as Float32Array
+                const db = ChunkedArray.compact(depths, true) as Float32Array
                 const ib = ChunkedArray.compact(indices, true) as Uint32Array
                 const gb = ChunkedArray.compact(groups, true) as Float32Array
                 const tb = ChunkedArray.compact(tcoords, true) as Float32Array
-                const ft = fontAtlas.texture
                 return {
                     kind: 'text',
                     charCount: indices.elementCount / 2,
                     fontTexture: text ? ValueCell.update(text.fontTexture, ft) : ValueCell.create(ft),
                     centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
                     mappingBuffer: text ? ValueCell.update(text.mappingBuffer, mb) : ValueCell.create(mb),
+                    depthBuffer: text ? ValueCell.update(text.depthBuffer, db) : ValueCell.create(db),
                     indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
                     groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
                     tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb),

+ 25 - 8
src/mol-geo/geometry/text/text.ts

@@ -24,6 +24,7 @@ import { RenderableState } from 'mol-gl/renderable';
 import { clamp } from 'mol-math/interpolate';
 import { createRenderObject as _createRenderObject } from 'mol-gl/render-object';
 import { BaseGeometry } from '../base';
+import { createEmptyOverpaint } from '../overpaint-data';
 
 type TextAttachment = (
     'bottom-left' | 'bottom-center' | 'bottom-right' |
@@ -44,6 +45,8 @@ export interface Text {
     readonly centerBuffer: ValueCell<Float32Array>,
     /** Mapping buffer as array of xy values wrapped in a value cell */
     readonly mappingBuffer: ValueCell<Float32Array>,
+    /** Depth buffer as array of z values wrapped in a value cell */
+    readonly depthBuffer: ValueCell<Float32Array>,
     /** Index buffer as array of center index triplets wrapped in a value cell */
     readonly indexBuffer: ValueCell<Uint32Array>,
     /** Group buffer as array of group ids for each vertex wrapped in a value cell */
@@ -54,18 +57,20 @@ export interface Text {
 
 export namespace Text {
     export function createEmpty(text?: Text): Text {
+        const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1)
         const cb = text ? text.centerBuffer.ref.value : new Float32Array(0)
         const mb = text ? text.mappingBuffer.ref.value : new Float32Array(0)
+        const db = text ? text.depthBuffer.ref.value : new Float32Array(0)
         const ib = text ? text.indexBuffer.ref.value : new Uint32Array(0)
         const gb = text ? text.groupBuffer.ref.value : new Float32Array(0)
         const tb = text ? text.tcoordBuffer.ref.value : new Float32Array(0)
-        const ft = text ? text.fontTexture.ref.value : createTextureImage(0, 1)
         return {
             kind: 'text',
             charCount: 0,
             fontTexture: text ? ValueCell.update(text.fontTexture, ft) : ValueCell.create(ft),
             centerBuffer: text ? ValueCell.update(text.centerBuffer, cb) : ValueCell.create(cb),
             mappingBuffer: text ? ValueCell.update(text.mappingBuffer, mb) : ValueCell.create(mb),
+            depthBuffer: text ? ValueCell.update(text.depthBuffer, db) : ValueCell.create(db),
             indexBuffer: text ? ValueCell.update(text.indexBuffer, ib) : ValueCell.create(ib),
             groupBuffer: text ? ValueCell.update(text.groupBuffer, gb) : ValueCell.create(gb),
             tcoordBuffer: text ? ValueCell.update(text.tcoordBuffer, tb) : ValueCell.create(tb)
@@ -86,6 +91,9 @@ export namespace Text {
         backgroundMargin: PD.Numeric(0.2, { min: 0, max: 1, step: 0.01 }),
         backgroundColor: PD.Color(ColorNames.grey),
         backgroundOpacity: PD.Numeric(1, { min: 0, max: 1, step: 0.01 }),
+        tether: PD.Boolean(false),
+        tetherLength: PD.Numeric(1, { min: 0, max: 5, step: 0.1 }),
+        tetherBaseWidth: PD.Numeric(0.3, { min: 0, max: 1, step: 0.01 }),
 
         attachment: PD.Select('middle-center', [
             ['bottom-left', 'bottom-left'], ['bottom-center', 'bottom-center'], ['bottom-right', 'bottom-right'],
@@ -115,10 +123,11 @@ export namespace Text {
         const color = createColors(locationIt, theme.color)
         const size = createSizes(locationIt, theme.size)
         const marker = createMarkers(instanceCount * groupCount)
+        const overpaint = createEmptyOverpaint()
 
         const counts = { drawCount: text.charCount * 2 * 3, groupCount, instanceCount }
 
-        const padding = getPadding(text.mappingBuffer.ref.value, text.charCount, getMaxSize(size))
+        const padding = getPadding(text.mappingBuffer.ref.value, text.depthBuffer.ref.value, text.charCount, getMaxSize(size))
         const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
             text.centerBuffer.ref.value, text.charCount * 4,
             transform.aTransform.ref.value, instanceCount, padding
@@ -127,6 +136,7 @@ export namespace Text {
         return {
             aPosition: text.centerBuffer,
             aMapping: text.mappingBuffer,
+            aDepth: text.depthBuffer,
             aGroup: text.groupBuffer,
             elements: text.indexBuffer,
             boundingSphere: ValueCell.create(boundingSphere),
@@ -134,6 +144,7 @@ export namespace Text {
             ...color,
             ...size,
             ...marker,
+            ...overpaint,
             ...transform,
 
             aTexCoord: text.tcoordBuffer,
@@ -155,7 +166,7 @@ export namespace Text {
 
     function createValuesSimple(text: Text, props: Partial<PD.Values<Params>>, colorValue: Color, sizeValue: number, transform?: TransformData) {
         const s = BaseGeometry.createSimple(colorValue, sizeValue, transform)
-        const p = { ...PD.getDefaultValues(Params), props }
+        const p = { ...PD.getDefaultValues(Params), ...props }
         return createValues(text, s.transform, s.locationIterator, s.theme, p)
     }
 
@@ -179,7 +190,7 @@ export namespace Text {
     }
 
     function updateBoundingSphere(values: TextValues, text: Text) {
-        const padding = getPadding(values.aMapping.ref.value, text.charCount, getMaxSize(values))
+        const padding = getPadding(values.aMapping.ref.value, values.aDepth.ref.value, text.charCount, getMaxSize(values))
         const { boundingSphere, invariantBoundingSphere } = calculateBoundingSphere(
             values.aPosition.ref.value, text.charCount * 4,
             values.aTransform.ref.value, values.instanceCount.ref.value, padding
@@ -205,13 +216,19 @@ export namespace Text {
     }
 }
 
-function getPadding(mapping: Float32Array, charCount: number, maxSize: number) {
+function getPadding(mappings: Float32Array, depths: Float32Array, charCount: number, maxSize: number) {
     let maxOffset = 0
+    let maxDepth = 0
     for (let i = 0, il = charCount * 4; i < il; ++i) {
-        const ox = Math.abs(mapping[i])
+        const i2 = 2 * i
+        const ox = Math.abs(mappings[i2])
         if (ox > maxOffset) maxOffset = ox
-        const oy = Math.abs(mapping[i + 1])
+        const oy = Math.abs(mappings[i2 + 1])
         if (oy > maxOffset) maxOffset = oy
+        const d = Math.abs(depths[i])
+        if (d > maxDepth) maxDepth = d
     }
-    return maxSize + maxSize * maxOffset
+    // console.log(maxDepth + maxSize, maxDepth, maxSize, maxSize + maxSize * maxOffset, depths)
+    return Math.max(maxDepth, maxSize + maxSize * maxOffset)
+    // return maxSize + maxSize * maxOffset + maxDepth
 }

+ 47 - 8
src/mol-geo/geometry/transform-data.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,8 +9,18 @@ import { Mat4 } from 'mol-math/linear-algebra';
 import { fillSerial } from 'mol-util/array';
 
 export type TransformData = {
+    /**
+     * final per-instance transform calculated for instance `i` as
+     * `aTransform[i] = matrix * transform[i] * extraTransform[i]`
+     */
     aTransform: ValueCell<Float32Array>,
+    /** global transform, see aTransform */
+    matrix: ValueCell<Mat4>,
+    /** base per-instance transform, see aTransform */
     transform: ValueCell<Float32Array>,
+    /** additional per-instance transform, see aTransform */
+    extraTransform: ValueCell<Float32Array>,
+
     uInstanceCount: ValueCell<number>,
     instanceCount: ValueCell<number>,
     aInstance: ValueCell<Float32Array>,
@@ -18,17 +28,30 @@ export type TransformData = {
 
 export function createTransform(transformArray: Float32Array, instanceCount: number, transformData?: TransformData): TransformData {
     if (transformData) {
-        ValueCell.update(transformData.aTransform, transformArray)
-        ValueCell.update(transformData.transform, new Float32Array(transformArray))
+        ValueCell.update(transformData.matrix, transformData.matrix.ref.value)
+        ValueCell.update(transformData.transform, transformArray)
         ValueCell.update(transformData.uInstanceCount, instanceCount)
         ValueCell.update(transformData.instanceCount, instanceCount)
+
+        const aTransform = transformData.aTransform.ref.value.length >= instanceCount * 16 ? transformData.aTransform.ref.value : new Float32Array(instanceCount * 16)
+        aTransform.set(transformArray)
+        ValueCell.update(transformData.aTransform, aTransform)
+
+        // Note that this sets `extraTransform` to identity transforms
+        const extraTransform = transformData.extraTransform.ref.value.length >= instanceCount * 16 ? transformData.extraTransform.ref.value : new Float32Array(instanceCount * 16)
+        ValueCell.update(transformData.extraTransform, fillIdentityTransform(extraTransform, instanceCount))
+
         const aInstance = transformData.aInstance.ref.value.length >= instanceCount ? transformData.aInstance.ref.value : new Float32Array(instanceCount)
         ValueCell.update(transformData.aInstance, fillSerial(aInstance, instanceCount))
+
+        updateTransformData(transformData)
         return transformData
     } else {
         return {
-            aTransform: ValueCell.create(transformArray),
-            transform: ValueCell.create(new Float32Array(transformArray)),
+            aTransform: ValueCell.create(new Float32Array(transformArray)),
+            matrix: ValueCell.create(Mat4.identity()),
+            transform: ValueCell.create(transformArray),
+            extraTransform: ValueCell.create(fillIdentityTransform(new Float32Array(instanceCount * 16), instanceCount)),
             uInstanceCount: ValueCell.create(instanceCount),
             instanceCount: ValueCell.create(instanceCount),
             aInstance: ValueCell.create(fillSerial(new Float32Array(instanceCount)))
@@ -38,16 +61,32 @@ export function createTransform(transformArray: Float32Array, instanceCount: num
 
 const identityTransform = new Float32Array(16)
 Mat4.toArray(Mat4.identity(), identityTransform, 0)
+
 export function createIdentityTransform(transformData?: TransformData): TransformData {
     return createTransform(new Float32Array(identityTransform), 1, transformData)
 }
 
-export function setTransformData(matrix: Mat4, transformData: TransformData) {
+export function fillIdentityTransform(transform: Float32Array, count: number) {
+    for (let i = 0; i < count; i++) {
+        transform.set(identityTransform, i * 16)
+    }
+    return transform
+}
+
+/**
+ * updates per-instance transform calculated for instance `i` as
+ * `aTransform[i] = matrix * transform[i] * extraTransform[i]`
+ */
+export function updateTransformData(transformData: TransformData) {
+    const aTransform = transformData.aTransform.ref.value
     const instanceCount = transformData.instanceCount.ref.value
+    const matrix = transformData.matrix.ref.value
     const transform = transformData.transform.ref.value
-    const aTransform = transformData.aTransform.ref.value
+    const extraTransform = transformData.extraTransform.ref.value
     for (let i = 0; i < instanceCount; i++) {
-        Mat4.mulOffset(aTransform, transform, matrix, i * 16, i * 16, 0)
+        const i16 = i * 16
+        Mat4.mulOffset(aTransform, extraTransform, transform, i16, i16, i16)
+        Mat4.mulOffset(aTransform, matrix, aTransform, i16, 0, i16)
     }
     ValueCell.update(transformData.aTransform, aTransform)
 }

+ 36 - 11
src/mol-geo/primitive/box.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,6 +7,7 @@
 import { Vec3 } from 'mol-math/linear-algebra'
 import { Primitive, PrimitiveBuilder } from './primitive';
 import { polygon } from './polygon'
+import { Cage, createCage } from './cage';
 
 const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
 const points = polygon(4, true)
@@ -20,25 +21,25 @@ function createBox(perforated: boolean): Primitive {
     // create sides
     for (let i = 0; i < 4; ++i) {
         const ni = (i + 1) % 4
-        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)
+        Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
+        Vec3.set(c, points[ni * 3], points[ni * 3 + 1], 0.5)
+        Vec3.set(d, points[i * 3], points[i * 3 + 1], 0.5)
         builder.add(a, b, c)
         if (!perforated) builder.add(c, d, a)
     }
 
     // create bases
     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)
+    Vec3.set(b, points[3], points[4], -0.5)
+    Vec3.set(c, points[6], points[7], -0.5)
+    Vec3.set(d, points[9], points[10], -0.5)
     builder.add(c, b, a)
     if (!perforated) builder.add(a, d, 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)
-    Vec3.set(d, points[6], points[7], 0.5)
+    Vec3.set(b, points[3], points[4], 0.5)
+    Vec3.set(c, points[6], points[7], 0.5)
+    Vec3.set(d, points[9], points[10], 0.5)
     builder.add(a, b, c)
     if (!perforated) builder.add(c, d, a)
 
@@ -55,4 +56,28 @@ let perforatedBox: Primitive
 export function PerforatedBox() {
     if (!perforatedBox) perforatedBox = createBox(true)
     return perforatedBox
+}
+
+let boxCage: Cage
+export function BoxCage() {
+    if (!boxCage) {
+        boxCage = createCage(
+            [
+                 0.5,  0.5, -0.5, // bottom
+                -0.5,  0.5, -0.5,
+                -0.5, -0.5, -0.5,
+                 0.5, -0.5, -0.5,
+                 0.5,  0.5, 0.5,  // top
+                -0.5,  0.5, 0.5,
+                -0.5, -0.5, 0.5,
+                 0.5, -0.5, 0.5
+            ],
+            [
+                0, 4,  1, 5,  2, 6,  3, 7, // sides
+                0, 1,  1, 2,  2, 3,  3, 0,  // bottom base
+                4, 5,  5, 6,  6, 7,  7, 4   // top base
+            ]
+        )
+    }
+    return boxCage
 }

+ 14 - 0
src/mol-geo/primitive/cage.ts

@@ -0,0 +1,14 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export interface Cage {
+    readonly vertices: ArrayLike<number>
+    readonly edges: ArrayLike<number>
+}
+
+export function createCage(vertices: ArrayLike<number>, edges: ArrayLike<number>): Cage {
+    return { vertices, edges }
+}

+ 69 - 0
src/mol-geo/primitive/dodecahedron.ts

@@ -0,0 +1,69 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createPrimitive, Primitive } from './primitive';
+import { Cage, createCage } from './cage';
+
+const t = (1 + Math.sqrt(5)) / 2;
+
+const a = 1;
+const b = 1 / t;
+const c = 2 - t;
+
+export const dodecahedronVertices: ReadonlyArray<number> = [
+     c, 0, a,    -c, 0, a,    -b, b, b,    0, a, c,     b, b, b,
+     b, -b, b,    0, -a, c,   -b, -b, b,   c, 0, -a,   -c, 0, -a,
+    -b, -b, -b,   0, -a, -c,   b, -b, -b,  b,  b, -b,   0, a, -c,
+    -b, b, -b,    a, c, 0,    -a, c, 0,   -a, -c, 0,    a, -c, 0
+];
+
+/** indices of pentagonal faces, groups of five  */
+export const dodecahedronFaces: ReadonlyArray<number> = [
+     4, 3, 2, 1, 0,
+     7, 6, 5, 0, 1,
+    12, 11, 10, 9, 8,
+    15, 14, 13, 8, 9,
+    14, 3, 4, 16, 13,
+     3, 14, 15, 17, 2,
+    11, 6, 7, 18, 10,
+     6, 11, 12, 19, 5,
+     4, 0, 5, 19, 16,
+    12, 8, 13, 16, 19,
+    15, 9, 10, 18, 17,
+     7, 1, 2, 17, 18
+];
+
+const dodecahedronIndices: ReadonlyArray<number> = [  // pentagonal faces
+     4, 3, 2,     2, 1, 0,     4, 2, 0,    // 4, 3, 2, 1, 0
+     7, 6, 5,     5, 0, 1,     7, 5, 1,    // 7, 6, 5, 0, 1
+    12, 11, 10,  10, 9, 8,    12, 10, 8,   // 12, 11, 10, 9, 8
+    15, 14, 13,  13, 8, 9,    15, 13, 9,   // 15, 14, 13, 8, 9
+    14, 3, 4,     4, 16, 13,  14, 4, 13,   // 14, 3, 4, 16, 13
+     3, 14, 15,   15, 17, 2,   3, 15, 2,   // 3, 14, 15, 17, 2
+    11, 6, 7,     7, 18, 10,  11, 7, 10,   // 11, 6, 7, 18, 10
+     6, 11, 12,  12, 19, 5,    6, 12, 5,   // 6, 11, 12, 19, 5
+     4, 0, 5,     5, 19, 16,   4, 5, 16,   // 4, 0, 5, 19, 16
+    12, 8, 13,   13, 16, 19,  12, 13, 19,  // 12, 8, 13, 16, 19
+    15, 9, 10,   10, 18, 17,  15, 10, 17,  // 15, 9, 10, 18, 17
+     7, 1, 2,     2, 17, 18,   7, 2, 18,   // 7, 1, 2, 17, 18
+];
+
+const dodecahedronEdges: ReadonlyArray<number> = [
+     0, 1,   0, 4,    0, 5,    1, 2,    1, 7,    2, 3,    2, 17,   3, 4,    3, 14,   4, 16,
+     5, 6,   5, 19,   6, 7,    6, 11,   7, 18,   8, 9,    8, 12,   8, 13,   9, 10,   9, 15,
+    10, 11, 10, 18,  11, 12,  12, 19,  13, 14,  13, 16,  14, 15,  15, 17,  16, 19,  17, 18,
+]
+
+let dodecahedron: Primitive
+export function Dodecahedron(): Primitive {
+    if (!dodecahedron) dodecahedron = createPrimitive(dodecahedronVertices, dodecahedronIndices)
+    return dodecahedron
+}
+
+const dodecahedronCage = createCage(dodecahedronVertices, dodecahedronEdges)
+export function DodecahedronCage(): Cage {
+    return dodecahedronCage
+}

+ 17 - 3
src/mol-geo/primitive/icosahedron.ts

@@ -5,8 +5,9 @@
  */
 
 import { createPrimitive, Primitive } from './primitive';
+import { Cage, createCage } from './cage';
 
-const t = ( 1 + Math.sqrt( 5 ) ) / 2;
+const t = (1 + Math.sqrt(5)) / 2;
 
 const icosahedronVertices: ReadonlyArray<number> = [
     -1, t, 0,   1, t, 0,  -1, -t, 0,   1, -t, 0,
@@ -21,6 +22,19 @@ const icosahedronIndices: ReadonlyArray<number> = [
     4, 9, 5,   2, 4, 11,   6, 2, 10,   8, 6, 7,   9, 8, 1
 ];
 
-const icosahedron = createPrimitive(icosahedronVertices, icosahedronIndices)
+const icosahedronEdges: ReadonlyArray<number> = [
+    0, 11,  5, 11,  0, 5,   1, 5,  0, 1,  1, 7,  0, 7,   7, 10,  0, 10,  10, 11,
+    5, 9,   4, 11,  2, 10,  6, 7,  1, 8,  3, 9,  4, 9,   3, 4,   2, 4,   2, 3,
+    2, 6,   3, 6,   6, 8,   3, 8,  8, 9,  4, 5,  2, 11,  6, 10,  7, 8,   1, 9
+]
 
-export function Icosahedron(): Primitive { return icosahedron }
+let icosahedron: Primitive
+export function Icosahedron(): Primitive {
+    if (!icosahedron) icosahedron = createPrimitive(icosahedronVertices, icosahedronIndices)
+    return icosahedron
+}
+
+const icosahedronCage = createCage(icosahedronVertices, icosahedronEdges)
+export function IcosahedronCage(): Cage {
+    return icosahedronCage
+}

+ 26 - 6
src/mol-geo/primitive/octahedron.ts

@@ -1,20 +1,23 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { createPrimitive, Primitive } from './primitive';
+import { createCage, Cage } from './cage';
 
 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
+    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
 ];
+
 export const perforatedOctahedronIndices: ReadonlyArray<number> = [
     0, 2, 4,   0, 4, 3,
     // 0, 3, 5,   0, 5, 2,
@@ -22,8 +25,25 @@ export const perforatedOctahedronIndices: ReadonlyArray<number> = [
     // 1, 3, 4,   1, 4, 2
 ];
 
-const octahedron = createPrimitive(octahedronVertices, octahedronIndices)
-const perforatedOctahedron = createPrimitive(octahedronVertices, perforatedOctahedronIndices)
+const octahedronEdges: ReadonlyArray<number> = [
+    0, 2,  1, 3,  2, 1,  3, 0,
+    0, 4,  1, 4,  2, 4,  3, 4,
+    0, 5,  1, 5,  2, 5,  3, 5,
+]
+
+let octahedron: Primitive
+export function Octahedron(): Primitive {
+    if (!octahedron) octahedron = createPrimitive(octahedronVertices, octahedronIndices)
+    return octahedron
+}
+
+let perforatedOctahedron: Primitive
+export function PerforatedOctahedron(): Primitive {
+    if (!perforatedOctahedron) perforatedOctahedron = createPrimitive(octahedronVertices, perforatedOctahedronIndices)
+    return perforatedOctahedron
+}
 
-export function Octahedron(): Primitive { return octahedron }
-export function PerforatedOctahedron(): Primitive { return perforatedOctahedron }
+const octahedronCage = createCage(octahedronVertices, octahedronEdges)
+export function OctahedronCage(): Cage {
+    return octahedronCage
+}

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

@@ -1,4 +1,5 @@
 import { Primitive } from './primitive';
+import { Cage } from './cage';
 
 /**
  * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
@@ -25,6 +26,15 @@ const plane: Primitive = {
     ])
 }
 
+const planeCage: Cage = {
+    vertices: plane.vertices,
+    edges: new Uint32Array([ 0, 1,  2, 3,  3, 1,  2, 0 ])
+}
+
 export function Plane(): Primitive {
     return plane
+}
+
+export function PlaneCage(): Cage {
+    return planeCage
 }

+ 8 - 7
src/mol-geo/primitive/polygon.ts

@@ -1,23 +1,24 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 /**
- * Create points for a polygon:
+ * Create 3d 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 points = new Float32Array(sideCount * 3)
     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
+    for (let i = 0, il = sideCount; i < il; ++i) {
+        const c = (i * 2 + offset) / sideCount * Math.PI
+        points[i * 3] = Math.cos(c) * radius
+        points[i * 3 + 1] = Math.sin(c) * radius
+        points[i * 3 + 2] = 0
     }
     return points
 }

+ 67 - 12
src/mol-geo/primitive/prism.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,16 +7,17 @@
 import { Vec3 } from 'mol-math/linear-algebra'
 import { Primitive, PrimitiveBuilder } from './primitive';
 import { polygon } from './polygon'
+import { Cage } from './cage';
 
 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 of 5 or more points
+ * Create a prism with a base of 4 or more points
  */
 export function Prism(points: ArrayLike<number>): Primitive {
-    const sideCount = points.length / 2
-    if (sideCount < 4) throw new Error('need at least 5 points to build a prism')
+    const sideCount = points.length / 3
+    if (sideCount < 4) throw new Error('need at least 4 points to build a prism')
 
     const count = 4 * sideCount
     const builder = PrimitiveBuilder(count)
@@ -24,10 +25,10 @@ export function Prism(points: ArrayLike<number>): Primitive {
     // 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)
+        Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
+        Vec3.set(c, points[ni * 3], points[ni * 3 + 1], 0.5)
+        Vec3.set(d, points[i * 3], points[i * 3 + 1], 0.5)
         builder.add(a, b, c)
         builder.add(c, d, a)
     }
@@ -35,11 +36,11 @@ export function Prism(points: ArrayLike<number>): Primitive {
     // create bases
     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(a, points[i * 3], points[i * 3 + 1], -0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
         builder.add(on, b, a)
-        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(a, points[i * 3], points[i * 3 + 1], 0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], 0.5)
         builder.add(a, b, op)
     }
 
@@ -62,4 +63,58 @@ let hexagonalPrism: Primitive
 export function HexagonalPrism() {
     if (!hexagonalPrism) hexagonalPrism = Prism(polygon(6, true))
     return hexagonalPrism
+}
+
+//
+
+/**
+ * Create a prism cage
+ */
+export function PrismCage(points: ArrayLike<number>): Cage {
+    const sideCount = points.length / 3
+
+    // const count = 4 * sideCount
+    const vertices: number[] = []
+    const edges: number[] = []
+
+    let offset = 0
+
+    // vertices and side edges
+    for (let i = 0; i < sideCount; ++i) {
+        vertices.push(
+            points[i * 3], points[i * 3 + 1], -0.5,
+            points[i * 3], points[i * 3 + 1], 0.5
+        )
+        edges.push(offset, offset + 1)
+        offset += 2
+    }
+
+    // bases edges
+    for (let i = 0; i < sideCount; ++i) {
+        const ni = (i + 1) % sideCount
+        edges.push(
+            i * 2, ni * 2,
+            i * 2 + 1, ni * 2 + 1
+        )
+    }
+
+    return { vertices, edges }
+}
+
+let diamondCage: Cage
+export function DiamondPrismCage() {
+    if (!diamondCage) diamondCage = PrismCage(polygon(4, false))
+    return diamondCage
+}
+
+let pentagonalPrismCage: Cage
+export function PentagonalPrismCage() {
+    if (!pentagonalPrismCage) pentagonalPrismCage = PrismCage(polygon(5, false))
+    return pentagonalPrismCage
+}
+
+let hexagonalPrismCage: Cage
+export function HexagonalPrismCage() {
+    if (!hexagonalPrismCage) hexagonalPrismCage = PrismCage(polygon(6, true))
+    return hexagonalPrismCage
 }

+ 52 - 16
src/mol-geo/primitive/pyramid.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,15 +7,16 @@
 import { Vec3 } from 'mol-math/linear-algebra'
 import { Primitive, PrimitiveBuilder, createPrimitive } from './primitive';
 import { polygon } from './polygon'
+import { Cage } from './cage';
 
 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 pyramid with a poligonal base
+ * Create a pyramid with a polygonal base
  */
 export function Pyramid(points: ArrayLike<number>): Primitive {
-    const sideCount = points.length / 2
+    const sideCount = points.length / 3
     const baseCount = sideCount === 3 ? 1 : sideCount === 4 ? 2 : sideCount
     const count = 2 * baseCount + 2 * sideCount
     const builder = PrimitiveBuilder(count)
@@ -23,29 +24,29 @@ export function Pyramid(points: ArrayLike<number>): Primitive {
     // 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(a, points[i * 3], points[i * 3 + 1], -0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 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)
+        Vec3.set(b, points[3], points[4], -0.5)
+        Vec3.set(c, points[6], points[7], -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)
+        Vec3.set(b, points[3], points[4], -0.5)
+        Vec3.set(c, points[6], points[7], -0.5)
+        Vec3.set(d, points[9], points[10], -0.5)
         builder.add(c, b, a)
         builder.add(a, d, c)
     } 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)
+            Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
+            Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
             builder.add(on, b, a)
         }
     }
@@ -59,16 +60,14 @@ export function OctagonalPyramid() {
     return octagonalPyramid
 }
 
-//
-
 let perforatedOctagonalPyramid: Primitive
 export function PerforatedOctagonalPyramid() {
     if (!perforatedOctagonalPyramid) {
         const points = polygon(8, true)
         const vertices = new Float32Array(8 * 3 + 6)
         for (let i = 0; i < 8; ++i) {
-            vertices[i * 3] = points[i * 2]
-            vertices[i * 3 + 1] = points[i * 2 + 1]
+            vertices[i * 3] = points[i * 3]
+            vertices[i * 3 + 1] = points[i * 3 + 1]
             vertices[i * 3 + 2] = -0.5
         }
         vertices[8 * 3] = 0
@@ -84,4 +83,41 @@ export function PerforatedOctagonalPyramid() {
         perforatedOctagonalPyramid = createPrimitive(vertices, indices)
     }
     return perforatedOctagonalPyramid
+}
+
+//
+
+/**
+ * Create a prism cage
+ */
+export function PyramidCage(points: ArrayLike<number>): Cage {
+    const sideCount = points.length / 3
+
+    // const count = 4 * sideCount
+    const vertices: number[] = []
+    const edges: number[] = []
+
+    let offset = 1
+    vertices.push(op[0], op[1], op[2])
+
+    // vertices and side edges
+    for (let i = 0; i < sideCount; ++i) {
+        vertices.push(points[i * 3], points[i * 3 + 1], -0.5)
+        edges.push(0, offset)
+        offset += 1
+    }
+
+    // bases edges
+    for (let i = 0; i < sideCount; ++i) {
+        const ni = (i + 1) % sideCount
+        edges.push(i + 1, ni + 1)
+    }
+
+    return { vertices, edges }
+}
+
+let octagonalPyramidCage: Cage
+export function OctagonalPyramidCage() {
+    if (!octagonalPyramidCage) octagonalPyramidCage = PyramidCage(polygon(8, true))
+    return octagonalPyramidCage
 }

+ 62 - 0
src/mol-geo/primitive/spiked-ball.ts

@@ -0,0 +1,62 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createPrimitive, Primitive } from './primitive';
+import { dodecahedronVertices, dodecahedronFaces } from './dodecahedron';
+import { Vec3 } from 'mol-math/linear-algebra';
+
+function calcCenter(out: Vec3, ...vec3s: Vec3[]) {
+    Vec3.set(out, 0, 0, 0)
+    for (let i = 0, il = vec3s.length; i < il; ++i) {
+        Vec3.add(out, out, vec3s[i])
+    }
+    Vec3.scale(out, out, 1 / vec3s.length)
+    return out
+}
+
+const center = Vec3.zero()
+const dir = Vec3.zero()
+const tip = Vec3.zero()
+
+const vecA = Vec3.zero()
+const vecB = Vec3.zero()
+const vecC = Vec3.zero()
+const vecD = Vec3.zero()
+const vecE = Vec3.zero()
+
+/**
+ * Create a spiked ball derived from a dodecahedron
+ * @param radiusRatio ratio between inner radius (dodecahedron) and outher radius (spikes)
+ */
+export function SpikedBall(radiusRatio = 1): Primitive {
+    const vertices = dodecahedronVertices.slice(0)
+    const indices: number[] = []
+
+    let offset = vertices.length / 3
+
+    for (let i = 0, il = dodecahedronFaces.length; i < il; i += 5) {
+        Vec3.fromArray(vecA, dodecahedronVertices, dodecahedronFaces[i] * 3)
+        Vec3.fromArray(vecB, dodecahedronVertices, dodecahedronFaces[i + 1] * 3)
+        Vec3.fromArray(vecC, dodecahedronVertices, dodecahedronFaces[i + 2] * 3)
+        Vec3.fromArray(vecD, dodecahedronVertices, dodecahedronFaces[i + 3] * 3)
+        Vec3.fromArray(vecE, dodecahedronVertices, dodecahedronFaces[i + 4] * 3)
+
+        calcCenter(center, vecA, vecB, vecC, vecD, vecE)
+        Vec3.triangleNormal(dir, vecA, vecB, vecC)
+        Vec3.scaleAndAdd(tip, center, dir, radiusRatio)
+
+        Vec3.toArray(tip, vertices, offset * 3)
+        indices.push(offset, dodecahedronFaces[i], dodecahedronFaces[i + 1])
+        indices.push(offset, dodecahedronFaces[i + 1], dodecahedronFaces[i + 2])
+        indices.push(offset, dodecahedronFaces[i + 2], dodecahedronFaces[i + 3])
+        indices.push(offset, dodecahedronFaces[i + 3], dodecahedronFaces[i + 4])
+        indices.push(offset, dodecahedronFaces[i + 4], dodecahedronFaces[i])
+
+        offset += 1
+    }
+
+    return createPrimitive(vertices, indices)
+}

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

@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { createPrimitive, Primitive } from './primitive';
+import { createCage, Cage } from './cage';
+
+export const tetrahedronVertices: ReadonlyArray<number> = [
+    0.7071, 0, 0,  -0.3535, 0.6123, 0,  -0.3535, -0.6123, 0,
+    0, 0, 0.7071,  0, 0, -0.7071
+
+];
+
+export const tetrahedronIndices: ReadonlyArray<number> = [
+    4, 1, 0,  4, 2, 1,  4, 0, 2,
+    0, 1, 3,  1, 2, 3,  2, 0, 3,
+];
+
+const tetrahedronEdges: ReadonlyArray<number> = [
+    0, 1,  1, 2,  2, 0,
+    0, 3,  1, 3,  2, 3,
+    0, 4,  1, 4,  2, 4,
+]
+
+let tetrahedron: Primitive
+export function Tetrahedron(): Primitive {
+    if (!tetrahedron) tetrahedron = createPrimitive(tetrahedronVertices, tetrahedronIndices)
+    return tetrahedron
+}
+
+const tetrahedronCage = createCage(tetrahedronVertices, tetrahedronEdges)
+export function TetrahedronCage(): Cage {
+    return tetrahedronCage
+}

+ 18 - 10
src/mol-geo/primitive/wedge.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -7,12 +7,14 @@
 import { Vec3 } from 'mol-math/linear-algebra'
 import { Primitive, PrimitiveBuilder } from './primitive';
 import { polygon } from './polygon'
+import { PrismCage } from './prism';
+import { Cage } from './cage';
 
 const a = Vec3.zero(), b = Vec3.zero(), c = Vec3.zero(), d = Vec3.zero()
 const points = polygon(3, false)
 
 /**
- * Create a prism with a poligonal base
+ * Create a prism with a triangular base
  */
 export function createWedge(): Primitive {
     const builder = PrimitiveBuilder(8)
@@ -20,22 +22,22 @@ export function createWedge(): Primitive {
     // create sides
     for (let i = 0; i < 3; ++i) {
         const ni = (i + 1) % 3
-        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)
+        Vec3.set(a, points[i * 3], points[i * 3 + 1], -0.5)
+        Vec3.set(b, points[ni * 3], points[ni * 3 + 1], -0.5)
+        Vec3.set(c, points[ni * 3], points[ni * 3 + 1], 0.5)
+        Vec3.set(d, points[i * 3], points[i * 3 + 1], 0.5)
         builder.add(a, b, c)
         builder.add(c, d, a)
     }
 
     // create bases
     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(b, points[3], points[4], -0.5)
+    Vec3.set(c, points[6], points[7], -0.5)
     builder.add(c, b, 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(b, points[3], points[4], 0.5)
+    Vec3.set(c, points[6], points[7], 0.5)
     builder.add(a, b, c)
 
     return builder.getPrimitive()
@@ -45,4 +47,10 @@ let wedge: Primitive
 export function Wedge() {
     if (!wedge) wedge = createWedge()
     return wedge
+}
+
+let wedgeCage: Cage
+export function WedgeCage() {
+    if (!wedgeCage) wedgeCage = PrismCage(points)
+    return wedgeCage
 }

+ 9 - 1
src/mol-gl/_spec/renderer.spec.ts

@@ -22,6 +22,7 @@ import { createEmptyMarkers } from 'mol-geo/geometry/marker-data';
 import { fillSerial } from 'mol-util/array';
 import { Color } from 'mol-util/color';
 import { Sphere3D } from 'mol-math/geometry';
+import { createEmptyOverpaint } from 'mol-geo/geometry/overpaint-data';
 
 // function writeImage(gl: WebGLRenderingContext, width: number, height: number) {
 //     const pixels = new Uint8Array(width * height * 4)
@@ -52,11 +53,13 @@ function createPoints() {
     const color = createValueColor(Color(0xFF0000))
     const size = createValueSize(1)
     const marker = createEmptyMarkers()
+    const overpaint = createEmptyOverpaint()
 
     const aTransform = ValueCell.create(new Float32Array(16))
     const m4 = Mat4.identity()
     Mat4.toArray(m4, aTransform.ref.value, 0)
     const transform = ValueCell.create(new Float32Array(aTransform.ref.value))
+    const extraTransform = ValueCell.create(new Float32Array(aTransform.ref.value))
 
     const boundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2))
     const invariantBoundingSphere = ValueCell.create(Sphere3D.create(Vec3.zero(), 2))
@@ -69,6 +72,7 @@ function createPoints() {
         ...color,
         ...marker,
         ...size,
+        ...overpaint,
 
         uAlpha: ValueCell.create(1.0),
         uHighlightColor: ValueCell.create(Vec3.create(1.0, 0.4, 0.6)),
@@ -76,9 +80,12 @@ function createPoints() {
         uInstanceCount: ValueCell.create(1),
         uGroupCount: ValueCell.create(3),
 
+        alpha: ValueCell.create(1.0),
         drawCount: ValueCell.create(3),
         instanceCount: ValueCell.create(1),
+        matrix: ValueCell.create(m4),
         transform,
+        extraTransform,
         boundingSphere,
         invariantBoundingSphere,
 
@@ -90,6 +97,7 @@ function createPoints() {
     }
     const state: RenderableState = {
         visible: true,
+        alphaFactor: 1,
         pickable: true,
         opaque: true
     }
@@ -127,7 +135,7 @@ describe('renderer', () => {
 
         scene.add(points)
         expect(ctx.bufferCount).toBe(4);
-        expect(ctx.textureCount).toBe(3);
+        expect(ctx.textureCount).toBe(4);
         expect(ctx.vaoCount).toBe(4);
         expect(ctx.programCache.count).toBe(4);
         expect(ctx.shaderCache.count).toBe(8);

+ 6 - 1
src/mol-gl/renderable.ts

@@ -1,5 +1,5 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
@@ -9,11 +9,13 @@ import { RenderableValues, Values, RenderableSchema } from './renderable/schema'
 import { RenderVariant, RenderItem } from './webgl/render-item';
 import { ValueCell } from 'mol-util';
 import { idFactory } from 'mol-util/id-factory';
+import { clamp } from 'mol-math/interpolate';
 
 const getNextRenderableId = idFactory()
 
 export type RenderableState = {
     visible: boolean
+    alphaFactor: number
     pickable: boolean
     opaque: boolean
 }
@@ -36,6 +38,9 @@ export function createRenderable<T extends Values<RenderableSchema>>(renderItem:
         state,
 
         render: (variant: RenderVariant) => {
+            if (values.uAlpha && values.alpha) {
+                ValueCell.updateIfChanged(values.uAlpha, clamp(values.alpha.ref.value * state.alphaFactor, 0, 1))
+            }
             if (values.uPickable) {
                 ValueCell.updateIfChanged(values.uPickable, state.pickable ? 1 : 0)
             }

+ 11 - 1
src/mol-gl/renderable/direct-volume.ts

@@ -20,6 +20,10 @@ export const DirectVolumeSchema = {
     uMarkerTexDim: UniformSpec('v2'),
     tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
 
+    uOverpaintTexDim: UniformSpec('v2'),
+    tOverpaint: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
+    dOverpaint: DefineSpec('boolean'),
+
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
 
@@ -28,7 +32,13 @@ export const DirectVolumeSchema = {
 
     drawCount: ValueSpec('number'),
     instanceCount: ValueSpec('number'),
-    transform: AttributeSpec('float32', 16, 1),
+
+    alpha: ValueSpec('number'),
+
+    matrix: ValueSpec('m4'),
+    transform: ValueSpec('float32'),
+    extraTransform: ValueSpec('float32'),
+
     boundingSphere: ValueSpec('sphere'),
     invariantBoundingSphere: ValueSpec('sphere'),
 

+ 52 - 4
src/mol-gl/renderable/schema.ts

@@ -19,8 +19,8 @@ export type ValueKindType = {
     'boolean': string
     'any': any
 
+    'm4': Mat4,
     'float32': Float32Array
-
     'sphere': Sphere3D
 }
 export type ValueKind = keyof ValueKindType
@@ -77,6 +77,20 @@ export function splitValues(schema: RenderableSchema, values: RenderableValues)
     return { attributeValues, defineValues, textureValues, uniformValues }
 }
 
+export function splitKeys(schema: RenderableSchema) {
+    const attributeKeys: string[] = []
+    const defineKeys: string[] = []
+    const textureKeys: string[] = []
+    const uniformKeys: string[] = []
+    Object.keys(schema).forEach(k => {
+        if (schema[k].type === 'attribute') attributeKeys.push(k)
+        if (schema[k].type === 'define') defineKeys.push(k)
+        if (schema[k].type === 'texture') textureKeys.push(k)
+        if (schema[k].type === 'uniform') uniformKeys.push(k)
+    })
+    return { attributeKeys, defineKeys, textureKeys, uniformKeys }
+}
+
 export type Versions<T extends RenderableValues> = { [k in keyof T]: number }
 export function getValueVersions<T extends RenderableValues>(values: T) {
     const versions: Versions<any> = {}
@@ -186,26 +200,60 @@ export const SizeSchema = {
 export type SizeSchema = typeof SizeSchema
 export type SizeValues = Values<SizeSchema>
 
+export const MarkerSchema = {
+    uMarkerTexDim: UniformSpec('v2'),
+    tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
+}
+export type MarkerSchema = typeof MarkerSchema
+export type MarkerValues = Values<MarkerSchema>
+
+export const OverpaintSchema = {
+    uOverpaintTexDim: UniformSpec('v2'),
+    tOverpaint: TextureSpec('image-uint8', 'rgba', 'ubyte', 'nearest'),
+    dOverpaint: DefineSpec('boolean'),
+}
+export type OverpaintSchema = typeof OverpaintSchema
+export type OverpaintValues = Values<OverpaintSchema>
+
 export const BaseSchema = {
     ...ColorSchema,
+    ...MarkerSchema,
+    ...OverpaintSchema,
 
     aInstance: AttributeSpec('float32', 1, 1),
     aGroup: AttributeSpec('float32', 1, 0),
+    /**
+     * final per-instance transform calculated for instance `i` as
+     * `aTransform[i] = matrix * transform[i] * extraTransform[i]`
+     */
     aTransform: AttributeSpec('float32', 16, 1),
 
+    /**
+     * final alpha, calculated as `values.alpha * state.alpha`
+     */
     uAlpha: UniformSpec('f'),
     uInstanceCount: UniformSpec('i'),
     uGroupCount: UniformSpec('i'),
-    uMarkerTexDim: UniformSpec('v2'),
+
     uHighlightColor: UniformSpec('v3'),
     uSelectColor: UniformSpec('v3'),
 
-    tMarker: TextureSpec('image-uint8', 'alpha', 'ubyte', 'nearest'),
-
     drawCount: ValueSpec('number'),
     instanceCount: ValueSpec('number'),
+
+    /** base alpha, see uAlpha  */
+    alpha: ValueSpec('number'),
+
+    /** global transform, see aTransform */
+    matrix: ValueSpec('m4'),
+    /** base per-instance transform, see aTransform */
     transform: ValueSpec('float32'),
+    /** additional per-instance transform, see aTransform */
+    extraTransform: ValueSpec('float32'),
+
+    /** bounding sphere taking aTransform into account */
     boundingSphere: ValueSpec('sphere'),
+    /** bounding sphere NOT taking aTransform into account */
     invariantBoundingSphere: ValueSpec('sphere'),
 
     dUseFog: DefineSpec('boolean'),

+ 1 - 0
src/mol-gl/renderable/text.ts

@@ -16,6 +16,7 @@ export const TextSchema = {
     ...SizeSchema,
     aPosition: AttributeSpec('float32', 3, 0),
     aMapping: AttributeSpec('float32', 2, 0),
+    aDepth: AttributeSpec('float32', 1, 0),
     elements: ElementsSpec('uint32'),
 
     aTexCoord: AttributeSpec('float32', 2, 0),

+ 3 - 2
src/mol-gl/renderable/util.ts

@@ -29,9 +29,10 @@ export interface TextureVolume<T extends Uint8Array | Float32Array> {
     readonly depth: number
 }
 
-export function createTextureImage(n: number, itemSize: number): TextureImage<Uint8Array> {
+export function createTextureImage(n: number, itemSize: number, array?: Uint8Array): TextureImage<Uint8Array> {
     const { length, width, height } = calculateTextureInfo(n, itemSize)
-    return { array: new Uint8Array(length), width, height }
+    array = array && array.length >= length ? array : new Uint8Array(length)
+    return { array, width, height }
 }
 
 export function printTextureImage(textureImage: TextureImage<any>, scale = 1) {

+ 16 - 6
src/mol-gl/scene.ts

@@ -7,7 +7,7 @@
 import { Renderable } from './renderable'
 import { WebGLContext } from './webgl/context';
 import { RenderableValues, BaseValues } from './renderable/schema';
-import { RenderObject, createRenderable } from './render-object';
+import { RenderObject, createRenderable, GraphicsRenderObject } from './render-object';
 import { Object3D } from './object3d';
 import { Sphere3D } from 'mol-math/geometry';
 import { Vec3 } from 'mol-math/linear-algebra';
@@ -57,8 +57,8 @@ interface Scene extends Object3D {
     readonly renderables: ReadonlyArray<Renderable<RenderableValues & BaseValues>>
     readonly boundingSphere: Sphere3D
 
-    update: (keepBoundingSphere?: boolean) => void
-    add: (o: RenderObject) => void
+    update: (objects: ArrayLike<GraphicsRenderObject> | undefined, keepBoundingSphere: boolean) => void
+    add: (o: RenderObject) => Renderable<any>
     remove: (o: RenderObject) => void
     has: (o: RenderObject) => boolean
     clear: () => void
@@ -80,10 +80,18 @@ namespace Scene {
             get direction () { return object3d.direction },
             get up () { return object3d.up },
 
-            update: (keepBoundingSphere?: boolean) => {
+            update(objects, keepBoundingSphere) {
                 Object3D.update(object3d)
-                for (let i = 0, il = renderables.length; i < il; ++i) {
-                    renderables[i].update()
+                if (objects) {
+                    for (let i = 0, il = objects.length; i < il; ++i) {
+                        const o = renderableMap.get(objects[i]);
+                        if (!o) continue;
+                        o.update();
+                    }
+                } else {
+                    for (let i = 0, il = renderables.length; i < il; ++i) {
+                        renderables[i].update()
+                    }
                 }
                 if (!keepBoundingSphere) boundingSphereDirty = true
             },
@@ -94,8 +102,10 @@ namespace Scene {
                     renderables.sort(renderableSort)
                     renderableMap.set(o, renderable)
                     boundingSphereDirty = true
+                    return renderable;
                 } else {
                     console.warn(`RenderObject with id '${o.id}' already present`)
+                    return renderableMap.get(o)!
                 }
             },
             remove: (o: RenderObject) => {

+ 4 - 0
src/mol-gl/shader/chunks/assign-color-varying.glsl

@@ -12,4 +12,8 @@
     vColor = vec4(encodeFloatRGB(aInstance), 1.0);
 #elif defined(dColorType_groupPicking)
     vColor = vec4(encodeFloatRGB(aGroup), 1.0);
+#endif
+
+#ifdef dOverpaint
+    vOverpaint = readFromTexture(tOverpaint, aInstance * float(uGroupCount) + aGroup, uOverpaintTexDim);
 #endif

+ 5 - 0
src/mol-gl/shader/chunks/assign-material-color.glsl

@@ -4,4 +4,9 @@
     vec4 material = vec4(vColor.rgb, uAlpha);
 #elif defined(dColorType_objectPicking) || defined(dColorType_instancePicking) || defined(dColorType_groupPicking)
     vec4 material = uPickable == 1 ? vColor : vec4(0.0, 0.0, 0.0, 1.0); // set to empty picking id
+#endif
+
+// mix material with overpaint
+#if defined(dOverpaint) && (defined(dColorType_uniform) || defined(dColorType_attribute) || defined(dColorType_instance) || defined(dColorType_group) || defined(dColorType_groupInstance))
+    material.rgb = mix(material.rgb, vOverpaint.rgb, vOverpaint.a);
 #endif

+ 4 - 0
src/mol-gl/shader/chunks/color-frag-params.glsl

@@ -8,4 +8,8 @@
     #else
         flat in vec4 vColor;
     #endif
+#endif
+
+#ifdef dOverpaint
+    varying vec4 vOverpaint;
 #endif

+ 6 - 0
src/mol-gl/shader/chunks/color-vert-params.glsl

@@ -14,4 +14,10 @@
         flat out vec4 vColor;
     #endif
     #pragma glslify: encodeFloatRGB = require(../utils/encode-float-rgb.glsl)
+#endif
+
+#ifdef dOverpaint
+    varying vec4 vOverpaint;
+    uniform vec2 uOverpaintTexDim;
+    uniform sampler2D tOverpaint;
 #endif

+ 11 - 4
src/mol-gl/shader/text.vert

@@ -15,6 +15,7 @@ uniform mat4 uModelView;
 
 attribute vec3 aPosition;
 attribute vec2 aMapping;
+attribute float aDepth;
 attribute vec2 aTexCoord;
 attribute mat4 aTransform;
 attribute float aInstance;
@@ -43,13 +44,11 @@ void main(void){
 
     float offsetX = uOffsetX * scale;
     float offsetY = uOffsetY * scale;
-    float offsetZ = uOffsetZ * scale;
-    if (vTexCoord.x == 10.0) {
-        offsetZ -= 0.01;
-    }
+    float offsetZ = (uOffsetZ + aDepth * 0.95) * scale;
 
     vec4 mvPosition = uModelView * aTransform * vec4(aPosition, 1.0);
 
+    // TODO
     // #ifdef FIXED_SIZE
     //     if (ortho) {
     //         scale /= pixelRatio * ((uViewportHeight / 2.0) / -uCameraPosition.z) * 0.1;
@@ -59,9 +58,17 @@ void main(void){
     // #endif
 
     vec4 mvCorner = vec4(mvPosition.xyz, 1.0);
+
+    if (vTexCoord.x == 10.0) { // indicates background plane
+        // move a bit to the back, tkaing ditsnace to camera into account to avoid z-fighting
+        offsetZ -= 0.001 * distance(uCameraPosition, (uProjection * mvCorner).xyz);
+    }
+
     mvCorner.xy += aMapping * size * scale;
     mvCorner.x += offsetX;
     mvCorner.y += offsetY;
+
+    // TODO
     // if(ortho){
     //     mvCorner.xyz += normalize(-uCameraPosition) * offsetZ;
     // } else {

+ 33 - 21
src/mol-gl/webgl/render-item.ts

@@ -9,7 +9,7 @@ import { createTextures } from './texture';
 import { WebGLContext } from './context';
 import { ShaderCode } from '../shader-code';
 import { Program } from './program';
-import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values } from '../renderable/schema';
+import { RenderableSchema, RenderableValues, AttributeSpec, getValueVersions, splitValues, Values, splitKeys } from '../renderable/schema';
 import { idFactory } from 'mol-util/id-factory';
 import { deleteVertexArray, createVertexArray } from './vertex-array';
 import { ValueCell } from 'mol-util';
@@ -60,6 +60,22 @@ interface ValueChanges {
     textures: boolean
     uniforms: boolean
 }
+function createValueChanges() {
+    return {
+        attributes: false,
+        defines: false,
+        elements: false,
+        textures: false,
+        uniforms: false,
+    }
+}
+function resetValueChanges(valueChanges: ValueChanges) {
+    valueChanges.attributes = false
+    valueChanges.defines = false
+    valueChanges.elements = false
+    valueChanges.textures = false
+    valueChanges.uniforms = false
+}
 
 // TODO make `RenderVariantDefines` a parameter for `createRenderItem`
 
@@ -74,6 +90,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
     const { instancedArrays, vertexArrayObject } = ctx.extensions
 
     const { attributeValues, defineValues, textureValues, uniformValues } = splitValues(schema, values)
+    const { attributeKeys, defineKeys, textureKeys } = splitKeys(schema)
     const versions = getValueVersions(values)
 
     const glDrawMode = getDrawMode(ctx, drawMode)
@@ -109,13 +126,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
     ctx.instanceCount += instanceCount
     ctx.instancedDrawCount += instanceCount * drawCount
 
-    const valueChanges: ValueChanges = {
-        attributes: false,
-        defines: false,
-        elements: false,
-        textures: false,
-        uniforms: false
-    }
+    const valueChanges = createValueChanges()
 
     let destroyed = false
 
@@ -130,7 +141,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
             program.setUniforms(uniformValues)
             if (vertexArrayObject && vertexArray) {
                 vertexArrayObject.bindVertexArray(vertexArray)
-                // need to bind elements buffer explicitely since it is not always recorded in the VAO
+                // need to bind elements buffer explicitly since it is not always recorded in the VAO
                 if (elementsBuffer) elementsBuffer.bind()
             } else {
                 if (elementsBuffer) elementsBuffer.bind()
@@ -144,15 +155,17 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
             }
         },
         update: () => {
-            valueChanges.defines = false
-            Object.keys(defineValues).forEach(k => {
+            resetValueChanges(valueChanges)
+
+            for (let i = 0, il = defineKeys.length; i < il; ++i) {
+                const k = defineKeys[i]
                 const value = defineValues[k]
                 if (value.ref.version !== versions[k]) {
                     // console.log('define version changed', k)
                     valueChanges.defines = true
                     versions[k] = value.ref.version
                 }
-            })
+            }
 
             if (valueChanges.defines) {
                 // console.log('some defines changed, need to rebuild programs')
@@ -182,26 +195,25 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
                 versions.instanceCount = values.instanceCount.ref.version
             }
 
-            valueChanges.attributes = false
-            Object.keys(attributeValues).forEach(k => {
+            for (let i = 0, il = attributeKeys.length; i < il; ++i) {
+                const k = attributeKeys[i]
                 const value = attributeValues[k]
                 if (value.ref.version !== versions[k]) {
                     const buffer = attributeBuffers[k]
                     if (buffer.length >= value.ref.value.length) {
                         // console.log('attribute array large enough to update', k, value.ref.id, value.ref.version)
-                        attributeBuffers[k].updateData(value.ref.value)
+                        buffer.updateData(value.ref.value)
                     } else {
                         // console.log('attribute array to small, need to create new attribute', k, value.ref.id, value.ref.version)
-                        attributeBuffers[k].destroy()
+                        buffer.destroy()
                         const { itemSize, divisor } = schema[k] as AttributeSpec<ArrayKind>
                         attributeBuffers[k] = createAttributeBuffer(ctx, value.ref.value, itemSize, divisor)
                         valueChanges.attributes = true
                     }
                     versions[k] = value.ref.version
                 }
-            })
+            }
 
-            valueChanges.elements = false
             if (elementsBuffer && values.elements.ref.version !== versions.elements) {
                 if (elementsBuffer.length >= values.elements.ref.value.length) {
                     // console.log('elements array large enough to update', values.elements.ref.id, values.elements.ref.version)
@@ -232,8 +244,8 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
                 }
             }
 
-            valueChanges.textures = false
-            Object.keys(textureValues).forEach(k => {
+            for (let i = 0, il = textureKeys.length; i < il; ++i) {
+                const k = textureKeys[i]
                 const value = textureValues[k]
                 if (value.ref.version !== versions[k]) {
                     // update of textures with kind 'texture' is done externally
@@ -244,7 +256,7 @@ export function createRenderItem(ctx: WebGLContext, drawMode: DrawMode, shaderCo
                         valueChanges.textures = true
                     }
                 }
-            })
+            }
 
             return valueChanges
         },

+ 2 - 4
src/mol-gl/webgl/texture.ts

@@ -183,8 +183,7 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
             width = _width, height = _height, depth = _depth || 0
             gl.bindTexture(target, texture)
             if (target === gl.TEXTURE_2D) {
-                // TODO remove cast when webgl2 types are fixed
-                (gl as WebGLRenderingContext).texImage2D(target, 0, internalFormat, width, height, 0, format, type, null)
+                gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, null)
             } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D && depth !== undefined) {
                 (gl as WebGL2RenderingContext).texImage3D(target, 0, internalFormat, width, height, depth, 0, format, type, null)
             } else {
@@ -200,8 +199,7 @@ export function createTexture(ctx: WebGLContext, kind: TextureKind, _format: Tex
             if (target === gl.TEXTURE_2D) {
                 const { array, width: _width, height: _height } = data as TextureImage<any>
                 width = _width, height = _height;
-                // TODO remove cast when webgl2 types are fixed
-                (gl as WebGLRenderingContext).texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
+                gl.texImage2D(target, 0, internalFormat, width, height, 0, format, type, array)
             } else if (target === (gl as WebGL2RenderingContext).TEXTURE_3D) {
                 const { array, width: _width, height: _height, depth: _depth } = data as TextureVolume<any>
                 width = _width, height = _height, depth = _depth;

+ 108 - 22
src/mol-io/common/file-handle.ts

@@ -1,46 +1,132 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 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 { defaults } from 'mol-util';
+import { defaults, noop } from 'mol-util';
+import { SimpleBuffer } from './simple-buffer';
+// only import 'fs' in node.js
+const fs = typeof document === 'undefined' ? require('fs') as typeof import('fs') : void 0;
 
 export interface FileHandle {
-    /** The number of bytes in the file */
-    length: number
     /**
+     * Asynchronously reads data, returning buffer and number of bytes read
+     *
      * @param position The offset from the beginning of the file from which data should be read.
-     * @param sizeOrBuffer The buffer the data will be written to. If a number a buffer of that size will be created.
-     * @param size The number of bytes to read.
+     * @param sizeOrBuffer The buffer the data will be read from.
+     * @param length The number of bytes to read.
      * @param byteOffset The offset in the buffer at which to start writing.
      */
-    readBuffer(position: number, sizeOrBuffer: Uint8Array | number, size?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: Uint8Array }>
+    readBuffer(position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number): Promise<{ bytesRead: number, buffer: SimpleBuffer }>
+
+    /**
+     * Asynchronously writes buffer, returning the number of bytes written.
+     *
+     * @param position — The offset from the beginning of the file where this data should be written.
+     * @param buffer - The buffer data to be written.
+     * @param length — The number of bytes to write. If not supplied, defaults to buffer.length
+     */
+    writeBuffer(position: number, buffer: SimpleBuffer, length?: number): Promise<number>
+
+    /**
+     * Synchronously writes buffer, returning the number of bytes written.
+     *
+     * @param position — The offset from the beginning of the file where this data should be written.
+     * @param buffer - The buffer data to be written.
+     * @param length — The number of bytes to write. If not supplied, defaults to buffer.length
+     */
+    writeBufferSync(position: number, buffer: SimpleBuffer, length?: number): number
+
+    /** Closes a file handle */
+    close(): void
 }
 
 export namespace FileHandle {
-    export function fromBuffer(buffer: Uint8Array): FileHandle {
+    export function fromBuffer(buffer: SimpleBuffer): FileHandle {
         return {
-            length: buffer.length,
-            readBuffer: (position: number, sizeOrBuffer: Uint8Array | number, size?: number, byteOffset?: number) => {
+            readBuffer: (position: number, sizeOrBuffer: SimpleBuffer | number, size?: number, byteOffset?: number) => {
+                let bytesRead: number
+                let outBuffer: SimpleBuffer
                 if (typeof sizeOrBuffer === 'number') {
+                    size = defaults(size, sizeOrBuffer)
                     const start = position
-                    const end = Math.min(buffer.length, start + (defaults(size, sizeOrBuffer)))
-                    return Promise.resolve({
-                        bytesRead: end - start,
-                        buffer: buffer.subarray(start, end),
-                    })
+                    const end = Math.min(buffer.length, start + size)
+                    bytesRead = end - start
+                    outBuffer = SimpleBuffer.fromUint8Array(new Uint8Array(buffer.buffer, start, end - start))
                 } else {
-                    if (size === void 0) {
-                        return Promise.reject('readBuffer: Specify size.');
-                    }
+                    size = defaults(size, sizeOrBuffer.length)
                     const start = position
-                    const end = Math.min(buffer.length, start + defaults(size, sizeOrBuffer.length))
+                    const end = Math.min(buffer.length, start + size)
                     sizeOrBuffer.set(buffer.subarray(start, end), byteOffset)
-                    return Promise.resolve({
-                        bytesRead: end - start,
-                        buffer: sizeOrBuffer,
+                    bytesRead = end - start
+                    outBuffer = sizeOrBuffer
+                }
+                if (size !== bytesRead) {
+                    console.warn(`byteCount ${size} and bytesRead ${bytesRead} differ`)
+                }
+                return Promise.resolve({ bytesRead, buffer: outBuffer })
+            },
+            writeBuffer: (position: number, buffer: SimpleBuffer, length?: number) => {
+                length = defaults(length, buffer.length)
+                console.error('.writeBuffer not implemented for FileHandle.fromBuffer')
+                return Promise.resolve(0)
+            },
+            writeBufferSync: (position: number, buffer: SimpleBuffer, length?: number, ) => {
+                length = defaults(length, buffer.length)
+                console.error('.writeSync not implemented for FileHandle.fromBuffer')
+                return 0
+            },
+            close: noop
+        }
+    }
+
+    export function fromDescriptor(file: number): FileHandle {
+        if (fs === undefined) throw new Error('fs module not available')
+        return {
+            readBuffer: (position: number, sizeOrBuffer: SimpleBuffer | number, length?: number, byteOffset?: number) => {
+                return new Promise((res, rej) => {
+                    let outBuffer: SimpleBuffer
+                    if (typeof sizeOrBuffer === 'number') {
+                        byteOffset = defaults(byteOffset, 0)
+                        length = defaults(length, sizeOrBuffer)
+                        outBuffer = SimpleBuffer.fromArrayBuffer(new ArrayBuffer(sizeOrBuffer));
+                    } else {
+                        byteOffset = defaults(byteOffset, 0)
+                        length = defaults(length, sizeOrBuffer.length)
+                        outBuffer = sizeOrBuffer
+                    }
+                    fs.read(file, outBuffer, byteOffset, length, position, (err, bytesRead, buffer) => {
+                        if (err) {
+                            rej(err);
+                            return;
+                        }
+                        if (length !== bytesRead) {
+                            console.warn(`byteCount ${length} and bytesRead ${bytesRead} differ`)
+                        }
+                        res({ bytesRead, buffer });
+                    });
+                })
+            },
+            writeBuffer: (position: number, buffer: SimpleBuffer, length?: number) => {
+                length = defaults(length, buffer.length)
+                return new Promise<number>((res, rej) => {
+                    fs.write(file, buffer, 0, length, position, (err, written) => {
+                        if (err) rej(err);
+                        else res(written);
                     })
+                })
+            },
+            writeBufferSync: (position: number, buffer: Uint8Array, length?: number) => {
+                length = defaults(length, buffer.length)
+                return fs.writeSync(file, buffer, 0, length, position);
+            },
+            close: () => {
+                try {
+                    if (file !== void 0) fs.close(file, noop);
+                } catch (e) {
+
                 }
             }
         }

+ 127 - 0
src/mol-io/common/simple-buffer.ts

@@ -0,0 +1,127 @@
+/**
+ * Copyright (c) 2019 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 { defaults } from 'mol-util';
+
+export interface SimpleBuffer extends Uint8Array {
+    readInt8: (offset: number) => number
+    readUInt8: (offset: number) => number
+
+    writeInt8: (value: number, offset: number) => void
+    writeUInt8: (value: number, offset: number) => void
+
+    readInt16LE: (offset: number) => number
+    readInt32LE: (offset: number) => number
+    readUInt16LE: (offset: number) => number
+    readUInt32LE: (offset: number) => number
+    readFloatLE: (offset: number) => number
+    readDoubleLE: (offset: number) => number
+
+    writeInt16LE: (value: number, offset: number) => void
+    writeInt32LE: (value: number, offset: number) => void
+    writeUInt16LE: (value: number, offset: number) => void
+    writeUInt32LE: (value: number, offset: number) => void
+    writeFloatLE: (value: number, offset: number) => void
+    writeDoubleLE: (value: number, offset: number) => void
+
+    readInt16BE: (offset: number) => number
+    readInt32BE: (offset: number) => number
+    readUInt16BE: (offset: number) => number
+    readUInt32BE: (offset: number) => number
+    readFloatBE: (offset: number) => number
+    readDoubleBE: (offset: number) => number
+
+    writeInt16BE: (value: number, offset: number) => void
+    writeInt32BE: (value: number, offset: number) => void
+    writeUInt16BE: (value: number, offset: number) => void
+    writeUInt32BE: (value: number, offset: number) => void
+    writeFloatBE: (value: number, offset: number) => void
+    writeDoubleBE: (value: number, offset: number) => void
+
+    copy: (targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number) => number
+}
+
+export namespace SimpleBuffer {
+    export function fromUint8Array(array: Uint8Array): SimpleBuffer {
+        const dv = new DataView(array.buffer)
+        return Object.assign(array.subarray(0), {
+            readInt8: (offset: number) => dv.getInt8(offset),
+            readUInt8: (offset: number) => dv.getUint8(offset),
+            writeInt8: (value: number, offset: number) => dv.setInt8(offset, value),
+            writeUInt8: (value: number, offset: number) => dv.setUint8(offset, value),
+
+            readInt16LE: (offset: number) => dv.getInt16(offset, true),
+            readInt32LE: (offset: number) => dv.getInt32(offset, true),
+            readUInt16LE: (offset: number) => dv.getUint16(offset, true),
+            readUInt32LE: (offset: number) => dv.getUint32(offset, true),
+            readFloatLE: (offset: number) => dv.getFloat32(offset, true),
+            readDoubleLE: (offset: number) => dv.getFloat64(offset, true),
+
+            writeInt16LE: (value: number, offset: number) => dv.setInt16(offset, value, true),
+            writeInt32LE: (value: number, offset: number) => dv.setInt32(offset, value, true),
+            writeUInt16LE: (value: number, offset: number) => dv.setUint16(offset, value, true),
+            writeUInt32LE: (value: number, offset: number) => dv.setUint32(offset, value, true),
+            writeFloatLE: (value: number, offset: number) => dv.setFloat32(offset, value, true),
+            writeDoubleLE: (value: number, offset: number) => dv.setFloat64(offset, value, true),
+
+            readInt16BE: (offset: number) => dv.getInt16(offset, false),
+            readInt32BE: (offset: number) => dv.getInt32(offset, false),
+            readUInt16BE: (offset: number) => dv.getUint16(offset, false),
+            readUInt32BE: (offset: number) => dv.getUint32(offset, false),
+            readFloatBE: (offset: number) => dv.getFloat32(offset, false),
+            readDoubleBE: (offset: number) => dv.getFloat64(offset, false),
+
+            writeInt16BE: (value: number, offset: number) => dv.setInt16(offset, value, false),
+            writeInt32BE: (value: number, offset: number) => dv.setInt32(offset, value, false),
+            writeUInt16BE: (value: number, offset: number) => dv.setUint16(offset, value, false),
+            writeUInt32BE: (value: number, offset: number) => dv.setUint32(offset, value, false),
+            writeFloatBE: (value: number, offset: number) => dv.setFloat32(offset, value, false),
+            writeDoubleBE: (value: number, offset: number) => dv.setFloat64(offset, value, false),
+
+            copy: (targetBuffer: Uint8Array, targetStart?: number, sourceStart?: number, sourceEnd?: number) => {
+                targetStart = defaults(targetStart, 0)
+                sourceStart = defaults(sourceStart, 0)
+                sourceEnd = defaults(sourceEnd, array.length)
+                targetBuffer.set(array.subarray(sourceStart, sourceEnd), targetStart)
+                return sourceEnd - sourceStart
+            }
+        })
+    }
+
+    export function fromArrayBuffer(arrayBuffer: ArrayBuffer): SimpleBuffer {
+        return fromUint8Array(new Uint8Array(arrayBuffer))
+    }
+
+    export function fromBuffer(buffer: Buffer): SimpleBuffer {
+        return buffer
+    }
+
+    export const IsNativeEndianLittle = new Uint16Array(new Uint8Array([0x12, 0x34]).buffer)[0] === 0x3412;
+
+    /** source and target can't be the same */
+    export function flipByteOrder(source: SimpleBuffer, target: Uint8Array, byteCount: number, elementByteSize: number, offset: number) {
+        for (let i = 0, n = byteCount; i < n; i += elementByteSize) {
+            for (let j = 0; j < elementByteSize; j++) {
+                target[offset + i + elementByteSize - j - 1] = source[offset + i + j];
+            }
+        }
+    }
+
+    export function flipByteOrderInPlace2(buffer: ArrayBuffer, byteOffset = 0, length?: number) {
+        const intView = new Int16Array(buffer, byteOffset, length)
+        for (let i = 0, n = intView.length; i < n; ++i) {
+            const val = intView[i]
+            intView[i] = ((val & 0xff) << 8) | ((val >> 8) & 0xff)
+        }
+    }
+
+    export function ensureLittleEndian(source: SimpleBuffer, target: SimpleBuffer, byteCount: number, elementByteSize: number, offset: number) {
+        if (IsNativeEndianLittle) return;
+        if (!byteCount || elementByteSize <= 1) return;
+        flipByteOrder(source, target, byteCount, elementByteSize, offset);
+    }
+}

+ 73 - 0
src/mol-io/common/typed-array.ts

@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * Taken/adapted from DensityServer (https://github.com/dsehnal/DensityServer)
+ *
+ * @author David Sehnal <david.sehnal@gmail.com>
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { FileHandle } from 'mol-io/common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+
+export type TypedArrayValueType = 'float32' | 'int8' | 'int16'
+
+export namespace TypedArrayValueType {
+    export const Float32: TypedArrayValueType = 'float32';
+    export const Int8: TypedArrayValueType = 'int8';
+    export const Int16: TypedArrayValueType = 'int16';
+}
+
+export type TypedArrayValueArray = Float32Array | Int8Array | Int16Array
+
+export interface TypedArrayBufferContext {
+    type: TypedArrayValueType,
+    elementByteSize: number,
+    readBuffer: SimpleBuffer,
+    valuesBuffer: Uint8Array,
+    values: TypedArrayValueArray
+}
+
+export function getElementByteSize(type: TypedArrayValueType) {
+    if (type === TypedArrayValueType.Float32) return 4;
+    if (type === TypedArrayValueType.Int16) return 2;
+    return 1;
+}
+
+export function makeTypedArray(type: TypedArrayValueType, buffer: ArrayBuffer, byteOffset = 0, length?: number): TypedArrayValueArray {
+    if (type === TypedArrayValueType.Float32) return new Float32Array(buffer, byteOffset, length);
+    if (type === TypedArrayValueType.Int16) return new Int16Array(buffer, byteOffset, length);
+    return new Int8Array(buffer, byteOffset, length);
+}
+
+export function createTypedArray(type: TypedArrayValueType, size: number) {
+    switch (type) {
+        case TypedArrayValueType.Float32: return new Float32Array(new ArrayBuffer(4 * size));
+        case TypedArrayValueType.Int8: return new Int8Array(new ArrayBuffer(1 * size));
+        case TypedArrayValueType.Int16: return new Int16Array(new ArrayBuffer(2 * size));
+    }
+    throw Error(`${type} is not a supported value format.`);
+}
+
+export function createTypedArrayBufferContext(size: number, type: TypedArrayValueType): TypedArrayBufferContext {
+    let elementByteSize = getElementByteSize(type);
+    let arrayBuffer = new ArrayBuffer(elementByteSize * size);
+    let readBuffer = SimpleBuffer.fromArrayBuffer(arrayBuffer);
+    let valuesBuffer = SimpleBuffer.IsNativeEndianLittle ? arrayBuffer : new ArrayBuffer(elementByteSize * size);
+    return {
+        type,
+        elementByteSize,
+        readBuffer,
+        valuesBuffer: new Uint8Array(valuesBuffer),
+        values: makeTypedArray(type, valuesBuffer)
+    };
+}
+
+export async function readTypedArray(ctx: TypedArrayBufferContext, file: FileHandle, position: number, byteCount: number, valueByteOffset: number, littleEndian?: boolean) {
+    await file.readBuffer(position, ctx.readBuffer, byteCount, valueByteOffset);
+    if (ctx.elementByteSize > 1 && ((littleEndian !== void 0 && littleEndian !== SimpleBuffer.IsNativeEndianLittle) || !SimpleBuffer.IsNativeEndianLittle)) {
+        // fix the endian
+        SimpleBuffer.flipByteOrder(ctx.readBuffer, ctx.valuesBuffer, byteCount, ctx.elementByteSize, valueByteOffset);
+    }
+    return ctx.values;
+}

+ 2 - 4
src/mol-io/reader/_spec/ccp4.spec.ts

@@ -4,15 +4,13 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import CCP4 from '../ccp4/parser'
-import { FileHandle } from '../../common/file-handle';
+import * as CCP4 from '../ccp4/parser'
 
 const ccp4Buffer = new Uint8Array(4 * 64)
 
 describe('ccp4 reader', () => {
     it('basic', async () => {
-        const file = FileHandle.fromBuffer(ccp4Buffer)
-        const parsed = await CCP4(file).run();
+        const parsed = await CCP4.parse(ccp4Buffer).run();
 
         if (parsed.isError) {
             console.log(parsed)

+ 4 - 5
src/mol-io/reader/_spec/cif.spec.ts

@@ -6,17 +6,16 @@
  */
 
 import * as Data from '../cif/data-model'
-import TextField from '../cif/text/field'
 import * as Schema from '../cif/schema'
 import { Column } from 'mol-data/db'
 
 const columnData = `123abc d,e,f '4 5 6'`;
 // 123abc d,e,f '4 5 6'
 
-const intField = TextField({ data: columnData, indices: [0, 1, 1, 2, 2, 3], count: 3 }, 3);
-const strField = TextField({ data: columnData, indices: [3, 4, 4, 5, 5, 6], count: 3 }, 3);
-const strListField = TextField({ data: columnData, indices: [7, 12], count: 1 }, 1);
-const intListField = TextField({ data: columnData, indices: [14, 19], count: 1 }, 1);
+const intField = Data.CifField.ofTokens({ data: columnData, indices: [0, 1, 1, 2, 2, 3], count: 3 });
+const strField = Data.CifField.ofTokens({ data: columnData, indices: [3, 4, 4, 5, 5, 6], count: 3 });
+const strListField = Data.CifField.ofTokens({ data: columnData, indices: [7, 12], count: 1 });
+const intListField = Data.CifField.ofTokens({ data: columnData, indices: [14, 19], count: 1 });
 
 const testBlock = Data.CifBlock(['test'], {
     test: Data.CifCategory('test', 3, ['int', 'str', 'strList', 'intList'], {

+ 1 - 1
src/mol-io/reader/_spec/csv.spec.ts

@@ -62,7 +62,7 @@ describe('csv reader', () => {
     });
 
     it('tabs', async () => {
-        const parsed = await Csv(tabString, { delimiter: '\t' }).run();;
+        const parsed = await Csv(tabString, { delimiter: '\t' }).run();
         if (parsed.isError) return;
         const csvFile = parsed.result;
 

+ 3 - 3
src/mol-io/reader/_spec/gro.spec.ts

@@ -5,7 +5,7 @@
  * @author David Sehnal <david.sehnal@gmail.com>
  */
 
-import Gro from '../gro/parser'
+import { parseGRO } from '../gro/parser'
 
 const groString = `MD of 2 waters, t= 4.2
     6
@@ -26,7 +26,7 @@ const groStringHighPrecision = `Generated by trjconv : 2168 system t=  15.00000
 
 describe('gro reader', () => {
     it('basic', async () => {
-        const parsed = await Gro(groString).run();
+        const parsed = await parseGRO(groString).run();
 
         if (parsed.isError) {
             console.log(parsed)
@@ -57,7 +57,7 @@ describe('gro reader', () => {
     });
 
     it('high precision', async () => {
-        const parsed = await Gro(groStringHighPrecision).run();
+        const parsed = await parseGRO(groStringHighPrecision).run();
 
         if (parsed.isError) {
             console.log(parsed)

+ 115 - 71
src/mol-io/reader/ccp4/parser.ts

@@ -1,127 +1,171 @@
 /**
- * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ * Copyright (c) 2018-2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
 import { Task, RuntimeContext } from 'mol-task';
-import * as Schema from './schema'
-import Result from '../result'
+import { Ccp4File, Ccp4Header } from './schema'
+import { ReaderResult as Result } from '../result'
 import { FileHandle } from '../../common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+import { TypedArrayValueType, getElementByteSize, TypedArrayBufferContext, readTypedArray, createTypedArrayBufferContext } from 'mol-io/common/typed-array';
 
-async function parseInternal(file: FileHandle, ctx: RuntimeContext): Promise<Result<Schema.Ccp4File>> {
-    await ctx.update({ message: 'Parsing CCP4 file...' });
-
-    const { buffer } = await file.readBuffer(0, file.length)
-    const bin = buffer.buffer
-
-    const intView = new Int32Array(bin, 0, 56)
-    const floatView = new Float32Array(bin, 0, 56)
-    const dv = new DataView(bin)
+export async function readCcp4Header(file: FileHandle): Promise<{ header: Ccp4Header, littleEndian: boolean }> {
+    const headerSize = 1024;
+    const { buffer } = await file.readBuffer(0, headerSize)
 
     // 53  MAP         Character string 'MAP ' to identify file type
     const MAP = String.fromCharCode(
-        dv.getUint8(52 * 4), dv.getUint8(52 * 4 + 1),
-        dv.getUint8(52 * 4 + 2), dv.getUint8(52 * 4 + 3)
+        buffer.readUInt8(52 * 4), buffer.readUInt8(52 * 4 + 1),
+        buffer.readUInt8(52 * 4 + 2), buffer.readUInt8(52 * 4 + 3)
     )
     if (MAP !== 'MAP ') {
-        return Result.error('ccp4 format error, missing "MAP " string');
+        throw new Error('ccp4 format error, missing "MAP " string');
     }
 
     // 54  MACHST      Machine stamp indicating machine type which wrote file
     //                 17 and 17 for big-endian or 68 and 65 for little-endian
-    const MACHST = [ dv.getUint8(53 * 4), dv.getUint8(53 * 4 + 1) ]
+    const MACHST = [ buffer.readUInt8(53 * 4), buffer.readUInt8(53 * 4 + 1) ]
+    let littleEndian = true
     // found MRC files that don't have the MACHST stamp set and are big-endian
     if (MACHST[0] !== 68 && MACHST[1] !== 65) {
-        // flip byte order in-place
-        for (let i = 0, il = bin.byteLength; i < il; i += 4) {
-            dv.setFloat32(i, dv.getFloat32(i), true)
-        }
+        littleEndian = false;
     }
 
-    const header: Schema.Ccp4Header = {
-        NC: intView[0],
-        NR: intView[1],
-        NS: intView[2],
+    const readInt = littleEndian ? (o: number) => buffer.readInt32LE(o * 4) : (o: number) => buffer.readInt32BE(o * 4)
+    const readFloat = littleEndian ? (o: number) => buffer.readFloatLE(o * 4) : (o: number) => buffer.readFloatBE(o * 4)
+
+    const header: Ccp4Header = {
+        NC: readInt(0),
+        NR: readInt(1),
+        NS: readInt(2),
 
-        MODE: intView[3],
+        MODE: readInt(3),
 
-        NCSTART: intView[4],
-        NRSTART: intView[5],
-        NSSTART: intView[6],
+        NCSTART: readInt(4),
+        NRSTART: readInt(5),
+        NSSTART: readInt(6),
 
-        NX: intView[7],
-        NY: intView[8],
-        NZ: intView[9],
+        NX: readInt(7),
+        NY: readInt(8),
+        NZ: readInt(9),
 
-        xLength: floatView[10],
-        yLength: floatView[11],
-        zLength: floatView[12],
+        xLength: readFloat(10),
+        yLength: readFloat(11),
+        zLength: readFloat(12),
 
-        alpha: floatView[13],
-        beta: floatView[14],
-        gamma: floatView[15],
+        alpha: readFloat(13),
+        beta: readFloat(14),
+        gamma: readFloat(15),
 
-        MAPC: intView[16],
-        MAPR: intView[17],
-        MAPS: intView[18],
+        MAPC: readInt(16),
+        MAPR: readInt(17),
+        MAPS: readInt(18),
 
-        AMIN: floatView[19],
-        AMAX: floatView[20],
-        AMEAN: floatView[21],
+        AMIN: readFloat(19),
+        AMAX: readFloat(20),
+        AMEAN: readFloat(21),
 
-        ISPG: intView[22],
+        ISPG: readInt(22),
 
-        NSYMBT: intView[23],
+        NSYMBT: readInt(23),
 
-        LSKFLG: intView[24],
+        LSKFLG: readInt(24),
 
         SKWMAT: [], // TODO bytes 26-34
         SKWTRN: [], // TODO bytes 35-37
 
+        userFlag1: readInt(39),
+        userFlag2: readInt(40),
+
         // bytes 50-52 origin in X,Y,Z used for transforms
-        originX: floatView[49],
-        originY: floatView[50],
-        originZ: floatView[51],
+        originX: readFloat(49),
+        originY: readFloat(50),
+        originZ: readFloat(51),
 
         MAP, // bytes 53 MAP
         MACHST, // bytes 54 MACHST
 
-        ARMS: floatView[54],
+        ARMS: readFloat(54),
 
         // TODO bytes 56 NLABL
         // TODO bytes 57-256 LABEL
     }
 
-    const offset = 256 * 4 + header.NSYMBT
-    const count = header.NC * header.NR * header.NS
-    let values
-    if (header.MODE === 2) {
-        values = new Float32Array(bin, offset, count)
-    } else if (header.MODE === 0) {
-        values = new Int8Array(bin, offset, count)
-    } else {
-        return Result.error(`ccp4 mode '${header.MODE}' unsupported`);
-    }
+    return { header, littleEndian }
+}
 
-    // if the file was converted by mapmode2to0 - scale the data
-    // based on uglymol (https://github.com/uglymol/uglymol) by Marcin Wojdyr (wojdyr)
-    if (intView[39] === -128 && intView[40] === 127) {
-        values = new Float32Array(values)
+export async function readCcp4Slices(header: Ccp4Header, buffer: TypedArrayBufferContext, file: FileHandle, byteOffset: number, length: number, littleEndian: boolean) {
+    if (isMapmode2to0(header)) {
+        // data from mapmode2to0 is in MODE 0 (Int8) and needs to be scaled and written as float32
+        const valueByteOffset = 3 * length
+        // read int8 data to last quarter of the read buffer
+        await file.readBuffer(byteOffset, buffer.readBuffer, length, valueByteOffset);
+        // get int8 view of last quarter of the read buffer
+        const int8 = new Int8Array(buffer.valuesBuffer.buffer, valueByteOffset)
         // scaling f(x)=b1*x+b0 such that f(-128)=min and f(127)=max
         const b1 = (header.AMAX - header.AMIN) / 255.0
         const b0 = 0.5 * (header.AMIN + header.AMAX + b1)
-        for (let j = 0, jl = values.length; j < jl; ++j) {
-            values[j] = b1 * values[j] + b0
+        for (let j = 0, jl = length; j < jl; ++j) {
+            buffer.values[j] = b1 * int8[j] + b0
         }
+    } else {
+        await readTypedArray(buffer, file, byteOffset, length, 0, littleEndian);
+    }
+}
+
+function getCcp4DataType(mode: number) {
+    switch (mode) {
+        case 2: return TypedArrayValueType.Float32
+        case 1: return TypedArrayValueType.Int16
+        case 0: return TypedArrayValueType.Int8
     }
+    throw new Error(`ccp4 mode '${mode}' unsupported`);
+}
+
+/** check if the file was converted by mapmode2to0, see https://github.com/uglymol/uglymol */
+function isMapmode2to0(header: Ccp4Header) {
+    return header.userFlag1 === -128 && header.userFlag2 === 127
+}
 
-    const result: Schema.Ccp4File = { header, values };
-    return Result.success(result);
+export function getCcp4ValueType(header: Ccp4Header) {
+    return isMapmode2to0(header) ? TypedArrayValueType.Float32 : getCcp4DataType(header.MODE)
 }
 
-export function parse(file: FileHandle) {
-    return Task.create<Result<Schema.Ccp4File>>('Parse CCP4', ctx => parseInternal(file, ctx));
+export function getCcp4DataOffset(header: Ccp4Header) {
+    return 256 * 4 + header.NSYMBT
+}
+
+async function parseInternal(file: FileHandle, size: number, ctx: RuntimeContext): Promise<Ccp4File> {
+    await ctx.update({ message: 'Parsing CCP4/MRC/MAP file...' });
+
+    const { header, littleEndian } = await readCcp4Header(file)
+    const offset = getCcp4DataOffset(header)
+    const dataType = getCcp4DataType(header.MODE)
+    const valueType = getCcp4ValueType(header)
+
+    const count = header.NC * header.NR * header.NS
+    const elementByteSize = getElementByteSize(dataType)
+    const byteCount = count * elementByteSize
+
+    const buffer = createTypedArrayBufferContext(count, valueType)
+    readCcp4Slices(header, buffer, file, offset, byteCount, littleEndian)
+
+    const result: Ccp4File = { header, values: buffer.values };
+    return result
+}
+
+export function parseFile(file: FileHandle, size: number) {
+    return Task.create<Result<Ccp4File>>('Parse CCP4/MRC/MAP', async ctx => {
+        try {
+            return Result.success(await parseInternal(file, size, ctx));
+        } catch (e) {
+            return Result.error(e);
+        }
+    })
 }
 
-export default parse;
+export function parse(buffer: Uint8Array) {
+    return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length)
+}

+ 4 - 1
src/mol-io/reader/ccp4/schema.ts

@@ -81,6 +81,9 @@ export interface Ccp4Header {
      * May be used in CCP4 but not in MRC
      */
     SKWTRN: number[]
+    /** see https://github.com/uglymol/uglymol/blob/master/tools/mapmode2to0#L69 */
+    userFlag1: number,
+    userFlag2: number,
     /** x axis origin transformation (not used in CCP4) */
     originX: number
     /** y axis origin transformation (not used in CCP4) */
@@ -112,5 +115,5 @@ export interface Ccp4Header {
  */
 export interface Ccp4File {
     header: Ccp4Header
-    values: Float32Array | Int8Array
+    values: Float32Array | Int16Array | Int8Array
 }

+ 1 - 1
src/mol-io/reader/cif/binary/parser.ts

@@ -7,7 +7,7 @@
 import * as Data from '../data-model'
 import { EncodedCategory, EncodedFile } from '../../../common/binary-cif'
 import Field from './field'
-import Result from '../../result'
+import { ReaderResult as Result } from '../../result'
 import decodeMsgPack from '../../../common/msgpack/decode'
 import { Task } from 'mol-task'
 

+ 159 - 2
src/mol-io/reader/cif/data-model.ts

@@ -5,10 +5,12 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import { Column } from 'mol-data/db'
+import { Column, ColumnHelpers } from 'mol-data/db'
 import { Tensor } from 'mol-math/linear-algebra'
-import { getNumberType, NumberType } from '../common/text/number-parser';
+import { getNumberType, NumberType, parseInt as fastParseInt, parseFloat as fastParseFloat } from '../common/text/number-parser';
 import { Encoding } from '../../common/binary-cif';
+import { Tokens } from '../common/text/tokenizer';
+import { areValuesEqualProvider } from '../common/text/column/token';
 
 export interface CifFile {
     readonly name?: string,
@@ -55,6 +57,19 @@ export namespace CifCategory {
     export function empty(name: string): CifCategory {
         return { rowCount: 0, name, fieldNames: [], getField(name: string) { return void 0; } };
     };
+
+    export type SomeFields<S> = { [P in keyof S]?: CifField }
+    export type Fields<S> = { [P in keyof S]: CifField }
+
+    export function ofFields(name: string, fields: { [name: string]: CifField | undefined }): CifCategory {
+        const fieldNames = Object.keys(fields);
+        return {
+            rowCount: fieldNames.length > 0 ? fields[fieldNames[0]]!.rowCount : 0,
+            name,
+            fieldNames,
+            getField(name) { return fields[name]; }
+        };
+    }
 }
 
 /**
@@ -81,6 +96,148 @@ export interface CifField {
     toFloatArray(params?: Column.ToArrayParams<number>): ReadonlyArray<number>
 }
 
+export namespace CifField {
+    export function ofString(value: string) {
+        return ofStrings([value]);
+    }
+
+    export function ofStrings(values: string[]): CifField {
+        const rowCount = values.length;
+        const str: CifField['str'] = row => { const ret = values[row]; if (!ret || ret === '.' || ret === '?') return ''; return ret; };
+        const int: CifField['int'] = row => { const v = values[row]; return fastParseInt(v, 0, v.length) || 0; };
+        const float: CifField['float'] = row => { const v = values[row]; return fastParseFloat(v, 0, v.length) || 0; };
+        const valueKind: CifField['valueKind'] = row => {
+            const v = values[row], l = v.length;
+            if (l > 1) return Column.ValueKind.Present;
+            if (l === 0) return Column.ValueKind.NotPresent;
+            const c = v.charCodeAt(0);
+            if (c === 46 /* . */) return Column.ValueKind.NotPresent;
+            if (c === 63 /* ? */) return Column.ValueKind.Unknown;
+            return Column.ValueKind.Present;
+        };
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int,
+            float,
+            valueKind,
+            areValuesEqual: (rowA, rowB) => values[rowA] === values[rowB],
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+
+    export function ofNumbers(values: ArrayLike<number>): CifField {
+        const rowCount = values.length;
+        const str: CifField['str'] = row => { return '' + values[row]; };
+        const float: CifField['float'] = row => values[row];
+        const valueKind: CifField['valueKind'] = row => Column.ValueKind.Present;
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int: float,
+            float,
+            valueKind,
+            areValuesEqual: (rowA, rowB) => values[rowA] === values[rowB],
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+
+    export function ofTokens(tokens: Tokens): CifField {
+        const { data, indices, count: rowCount } = tokens;
+
+        const str: CifField['str'] = row => {
+            const ret = data.substring(indices[2 * row], indices[2 * row + 1]);
+            if (ret === '.' || ret === '?') return '';
+            return ret;
+        };
+
+        const int: CifField['int'] = row => {
+            return fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0;
+        };
+
+        const float: CifField['float'] = row => {
+            return fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
+        };
+
+        const valueKind: CifField['valueKind'] = row => {
+            const s = indices[2 * row], l = indices[2 * row + 1] - s;
+            if (l > 1) return Column.ValueKind.Present;
+            if (l === 0) return Column.ValueKind.NotPresent;
+            const v = data.charCodeAt(s);
+            if (v === 46 /* . */) return Column.ValueKind.NotPresent;
+            if (v === 63 /* ? */) return Column.ValueKind.Unknown;
+            return Column.ValueKind.Present;
+        };
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int,
+            float,
+            valueKind,
+            areValuesEqual: areValuesEqualProvider(tokens),
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+
+    export function ofColumn(column: Column<any>): CifField {
+        const { rowCount, valueKind, areValuesEqual } = column;
+        
+        let str: CifField['str']
+        let int: CifField['int']
+        let float: CifField['float']
+
+        switch (column.schema.valueType) {
+            case 'float':
+            case 'int':
+                str = row => { return '' + column.value(row); };
+                int = row => column.value(row);
+                float = row => column.value(row);
+                break
+            case 'str':
+                str = row => column.value(row);
+                int = row => { const v = column.value(row); return fastParseInt(v, 0, v.length) || 0; };
+                float = row => { const v = column.value(row); return fastParseFloat(v, 0, v.length) || 0; };
+                break
+            default:
+                throw new Error('unsupported')
+        }
+                
+
+        return {
+            __array: void 0,
+            binaryEncoding: void 0,
+            isDefined: true,
+            rowCount,
+            str,
+            int,
+            float,
+            valueKind,
+            areValuesEqual,
+            toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
+            toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
+            toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
+        }
+    }
+}
+
 export function getTensor(category: CifCategory, field: string, space: Tensor.Space, row: number, zeroIndexed: boolean): Tensor.Data {
     const ret = space.create();
     const offset = zeroIndexed ? 0 : 1;

+ 1 - 1
src/mol-io/reader/cif/schema/bird.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.303, IHM 0.139, CARB draft.
+ * Code-generated 'BIRD' schema file. Dictionary versions: mmCIF 5.305, IHM 0.139, CARB draft.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */

+ 1 - 1
src/mol-io/reader/cif/schema/ccd.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.303, IHM 0.139, CARB draft.
+ * Code-generated 'CCD' schema file. Dictionary versions: mmCIF 5.305, IHM 0.139, CARB draft.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */

+ 3 - 3
src/mol-io/reader/cif/schema/mmcif.ts

@@ -1,7 +1,7 @@
 /**
  * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  *
- * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.303, IHM 0.139, CARB draft.
+ * Code-generated 'mmCIF' schema file. Dictionary versions: mmCIF 5.305, IHM 0.139, CARB draft.
  *
  * @author mol-star package (src/apps/schema-generator/generate)
  */
@@ -491,7 +491,7 @@ export const mmCIF_Schema = {
          *
          * Corresponds to the compound name in the PDB format.
          */
-        pdbx_description: str,
+        pdbx_description: List(',', x => x),
         /**
          * A place holder for the number of molecules of the entity in
          * the entry.
@@ -850,7 +850,7 @@ export const mmCIF_Schema = {
          * This data item is a pointer to _struct_conn_type.id in the
          * STRUCT_CONN_TYPE category.
          */
-        conn_type_id: Aliased<'covale' | 'disulf' | 'hydrog' | 'metalc' | 'mismat' | 'saltbr' | 'modres' | 'covale_base' | 'covale_sugar' | 'covale_phosphate'>(str),
+        conn_type_id: Aliased<'covale' | 'disulf' | 'metalc' | 'hydrog'>(str),
         /**
          * A description of special aspects of the connection.
          */

+ 0 - 54
src/mol-io/reader/cif/text/field.ts

@@ -1,54 +0,0 @@
-/**
- * Copyright (c) 2017-2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
- *
- * @author David Sehnal <david.sehnal@gmail.com>
- * @author Alexander Rose <alexander.rose@weirdbyte.de>
- */
-
-import { Column, ColumnHelpers } from 'mol-data/db'
-import * as TokenColumn from '../../common/text/column/token'
-import { Tokens } from '../../common/text/tokenizer'
-import * as Data from '../data-model'
-import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../../common/text/number-parser'
-
-export default function CifTextField(tokens: Tokens, rowCount: number): Data.CifField {
-    const { data, indices } = tokens;
-
-    const str: Data.CifField['str'] = row => {
-        const ret = data.substring(indices[2 * row], indices[2 * row + 1]);
-        if (ret === '.' || ret === '?') return '';
-        return ret;
-    };
-
-    const int: Data.CifField['int'] = row => {
-        return fastParseInt(data, indices[2 * row], indices[2 * row + 1]) || 0;
-    };
-
-    const float: Data.CifField['float'] = row => {
-        return fastParseFloat(data, indices[2 * row], indices[2 * row + 1]) || 0;
-    };
-
-    const valueKind: Data.CifField['valueKind'] = row => {
-        const s = indices[2 * row];
-        if (indices[2 * row + 1] - s !== 1) return Column.ValueKind.Present;
-        const v = data.charCodeAt(s);
-        if (v === 46 /* . */) return Column.ValueKind.NotPresent;
-        if (v === 63 /* ? */) return Column.ValueKind.Unknown;
-        return Column.ValueKind.Present;
-    };
-
-    return {
-        __array: void 0,
-        binaryEncoding: void 0,
-        isDefined: true,
-        rowCount,
-        str,
-        int,
-        float,
-        valueKind,
-        areValuesEqual: TokenColumn.areValuesEqualProvider(tokens),
-        toStringArray: params => ColumnHelpers.createAndFillArray(rowCount, str, params),
-        toIntArray: params => ColumnHelpers.createAndFillArray(rowCount, int, params),
-        toFloatArray: params => ColumnHelpers.createAndFillArray(rowCount, float, params)
-    }
-}

+ 4 - 5
src/mol-io/reader/cif/text/parser.ts

@@ -23,9 +23,8 @@
  */
 
 import * as Data from '../data-model'
-import Field from './field'
 import { Tokens, TokenBuilder } from '../../common/text/tokenizer'
-import Result from '../../result'
+import { ReaderResult as Result } from '../../result'
 import { Task, RuntimeContext, chunkedSubtask } from 'mol-task'
 
 /**
@@ -445,7 +444,7 @@ function handleSingle(tokenizer: TokenizerState, ctx: FrameContext): CifCategory
                 errorMessage: 'Expected value.'
             }
         }
-        fields[fieldName] = Field({ data: tokenizer.data, indices: [tokenizer.tokenStart, tokenizer.tokenEnd], count: 1 }, 1);
+        fields[fieldName] = Data.CifField.ofTokens({ data: tokenizer.data, indices: [tokenizer.tokenStart, tokenizer.tokenEnd], count: 1 });
         fieldNames[fieldNames.length] = fieldName;
         moveNext(tokenizer);
     }
@@ -507,7 +506,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise
     const rowCountEstimate = name === '_atom_site' ? (tokenizer.data.length / 100) | 0 : 32;
     const tokens: Tokens[] = [];
     const fieldCount = fieldNames.length;
-    for (let i = 0; i < fieldCount; i++) tokens[i] = TokenBuilder.create(tokenizer, rowCountEstimate);
+    for (let i = 0; i < fieldCount; i++) tokens[i] = TokenBuilder.create(tokenizer.data, rowCountEstimate);
 
     const state: LoopReadState = {
         fieldCount,
@@ -529,7 +528,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise
     const rowCount = (state.tokenCount / fieldCount) | 0;
     const fields = Object.create(null);
     for (let i = 0; i < fieldCount; i++) {
-        fields[fieldNames[i]] = Field(tokens[i], rowCount);
+        fields[fieldNames[i]] = Data.CifField.ofTokens(tokens[i]);
     }
 
     const catName = name.substr(1);

+ 55 - 15
src/mol-io/reader/common/text/tokenizer.ts

@@ -8,7 +8,9 @@
 
 import { chunkedSubtask, RuntimeContext } from 'mol-task'
 
-export interface Tokenizer {
+export { Tokenizer }
+
+interface Tokenizer {
     data: string,
 
     position: number,
@@ -25,7 +27,7 @@ export interface Tokens {
     indices: ArrayLike<number>
 }
 
-export function Tokenizer(data: string): Tokenizer {
+function Tokenizer(data: string): Tokenizer {
     return {
         data,
         position: 0,
@@ -36,7 +38,7 @@ export function Tokenizer(data: string): Tokenizer {
     };
 }
 
-export namespace Tokenizer {
+namespace Tokenizer {
     export function getTokenString(state: Tokenizer) {
         return state.data.substring(state.tokenStart, state.tokenEnd);
     }
@@ -52,7 +54,7 @@ export namespace Tokenizer {
     /**
      * Eat everything until a newline occurs.
      */
-    export function eatLine(state: Tokenizer) {
+    export function eatLine(state: Tokenizer): boolean {
         const { data } = state;
         while (state.position < state.length) {
             switch (data.charCodeAt(state.position)) {
@@ -60,7 +62,7 @@ export namespace Tokenizer {
                     state.tokenEnd = state.position;
                     ++state.position;
                     ++state.lineNumber;
-                    return;
+                    return true;
                 case 13: // \r
                     state.tokenEnd = state.position;
                     ++state.position;
@@ -68,13 +70,14 @@ export namespace Tokenizer {
                     if (data.charCodeAt(state.position) === 10) {
                         ++state.position;
                     }
-                    return;
+                    return true;
                 default:
                     ++state.position;
                     break;
             }
         }
         state.tokenEnd = state.position;
+        return state.tokenStart !== state.tokenEnd;
     }
 
     /** Sets the current token start to the current position */
@@ -85,7 +88,7 @@ export namespace Tokenizer {
     /** Sets the current token start to current position and moves to the next line. */
     export function markLine(state: Tokenizer) {
         state.tokenStart = state.position;
-        eatLine(state);
+        return eatLine(state);
     }
 
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
@@ -95,15 +98,18 @@ export namespace Tokenizer {
     }
 
     function readLinesChunk(state: Tokenizer, count: number, tokens: Tokens) {
+        let read = 0;
         for (let i = 0; i < count; i++) {
-            markLine(state);
+            if (!markLine(state)) return read;
             TokenBuilder.addUnchecked(tokens, state.tokenStart, state.tokenEnd);
+            read++;
         }
+        return read;
     }
 
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
     export function readLines(state: Tokenizer, count: number): Tokens {
-        const lineTokens = TokenBuilder.create(state, count * 2);
+        const lineTokens = TokenBuilder.create(state.data, count * 2);
         readLinesChunk(state, count, lineTokens);
         return lineTokens;
     }
@@ -111,7 +117,7 @@ export namespace Tokenizer {
     /** Advance the state by the given number of lines and return line starts/ends as tokens. */
     export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> {
         const { length } = state;
-        const lineTokens = TokenBuilder.create(state, count * 2);
+        const lineTokens = TokenBuilder.create(state.data, count * 2);
 
         let linesAlreadyRead = 0;
         await chunkedSubtask(ctx, initialLineCount, state, (chunkSize, state) => {
@@ -124,6 +130,37 @@ export namespace Tokenizer {
         return lineTokens;
     }
 
+    export function readAllLines(data: string) {
+        const state = Tokenizer(data);
+        const tokens = TokenBuilder.create(state.data, Math.max(data.length / 80, 2))
+        while (markLine(state)) {
+            TokenBuilder.add(tokens, state.tokenStart, state.tokenEnd);
+        }
+        return tokens;
+    }
+
+    function readLinesChunkChecked(state: Tokenizer, count: number, tokens: Tokens) {
+        let read = 0;
+        for (let i = 0; i < count; i++) {
+            if (!markLine(state)) return read;
+            TokenBuilder.add(tokens, state.tokenStart, state.tokenEnd);
+            read++;
+        }
+        return read;
+    }
+
+    export async function readAllLinesAsync(data: string, ctx: RuntimeContext, chunkSize = 100000) {
+        const state = Tokenizer(data);
+        const tokens = TokenBuilder.create(state.data, Math.max(data.length / 80, 2));
+
+        await chunkedSubtask(ctx, chunkSize, state, (chunkSize, state) => {
+            readLinesChunkChecked(state, chunkSize, tokens);
+            return state.position < state.length ? chunkSize : 0;
+        }, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length }));
+
+        return tokens;
+    }
+
     /**
      * Eat everything until a whitespace/newline occurs.
      */
@@ -191,6 +228,7 @@ export namespace Tokenizer {
         state.tokenStart = s;
         state.tokenEnd = e + 1;
         state.position = end;
+        return state;
     }
 }
 
@@ -228,22 +266,24 @@ export namespace TokenBuilder {
         tokens.count++;
     }
 
+    export function addToken(tokens: Tokens, tokenizer: Tokenizer) {
+        add(tokens, tokenizer.tokenStart, tokenizer.tokenEnd);
+    }
+
     export function addUnchecked(tokens: Tokens, start: number, end: number) {
         (tokens as Builder).indices[(tokens as Builder).offset++] = start;
         (tokens as Builder).indices[(tokens as Builder).offset++] = end;
         tokens.count++;
     }
 
-    export function create(tokenizer: Tokenizer, size: number): Tokens {
+    export function create(data: string, size: number): Tokens {
         size = Math.max(10, size)
         return <Builder>{
-            data: tokenizer.data,
+            data,
             indicesLenMinus2: (size - 2) | 0,
             count: 0,
             offset: 0,
             indices: new Uint32Array(size)
         }
     }
-}
-
-export default Tokenizer
+}

+ 2 - 2
src/mol-io/reader/csv/field.ts

@@ -4,6 +4,6 @@
  * @author Alexander Rose <alexander.rose@weirdbyte.de>
  */
 
-import Field from '../cif/text/field'
+import { CifField } from '../cif/data-model';
 
-export default Field
+export default CifField.ofTokens

+ 3 - 3
src/mol-io/reader/csv/parser.ts

@@ -8,7 +8,7 @@
 import { Tokens, TokenBuilder, Tokenizer } from '../common/text/tokenizer'
 import * as Data from './data-model'
 import Field from './field'
-import Result from '../result'
+import { ReaderResult as Result } from '../result'
 import { Task, RuntimeContext, chunkedSubtask, } from 'mol-task'
 
 const enum CsvTokenType {
@@ -231,7 +231,7 @@ function readRecordsChunks(state: State) {
 
 function addColumn (state: State) {
     state.columnNames.push(Tokenizer.getTokenString(state.tokenizer))
-    state.tokens.push(TokenBuilder.create(state.tokenizer, state.data.length / 80))
+    state.tokens.push(TokenBuilder.create(state.tokenizer.data, state.data.length / 80))
 }
 
 function init(state: State) {
@@ -254,7 +254,7 @@ async function handleRecords(state: State): Promise<Data.CsvTable> {
 
     const columns: Data.CsvColumns = Object.create(null);
     for (let i = 0; i < state.columnCount; ++i) {
-        columns[state.columnNames[i]] = Field(state.tokens[i], state.recordCount);
+        columns[state.columnNames[i]] = Field(state.tokens[i]);
     }
 
     return Data.CsvTable(state.recordCount, state.columnNames, columns)

+ 153 - 0
src/mol-io/reader/dsn6/parser.ts

@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+import { Task, RuntimeContext } from 'mol-task';
+import { Dsn6File, Dsn6Header } from './schema'
+import { ReaderResult as Result } from '../result'
+import { FileHandle } from '../../common/file-handle';
+import { SimpleBuffer } from 'mol-io/common/simple-buffer';
+
+export const dsn6HeaderSize = 512;
+
+function parseBrixHeader(str: string): Dsn6Header {
+    return {
+        xStart: parseInt(str.substr(10, 5)),
+        yStart: parseInt(str.substr(15, 5)),
+        zStart: parseInt(str.substr(20, 5)),
+        xExtent: parseInt(str.substr(32, 5)),
+        yExtent: parseInt(str.substr(38, 5)),
+        zExtent: parseInt(str.substr(42, 5)),
+        xRate: parseInt(str.substr(52, 5)),
+        yRate: parseInt(str.substr(58, 5)),
+        zRate: parseInt(str.substr(62, 5)),
+        xlen: parseFloat(str.substr(73, 10)),
+        ylen: parseFloat(str.substr(83, 10)),
+        zlen: parseFloat(str.substr(93, 10)),
+        alpha: parseFloat(str.substr(103, 10)),
+        beta: parseFloat(str.substr(113, 10)),
+        gamma: parseFloat(str.substr(123, 10)),
+        divisor: parseFloat(str.substr(138, 12)),
+        summand: parseInt(str.substr(155, 8)),
+        sigma: parseFloat(str.substr(170, 12))
+    }
+}
+
+function parseDsn6Header(buffer: SimpleBuffer, littleEndian: boolean): Dsn6Header {
+    const readInt = littleEndian ? (o: number) => buffer.readInt16LE(o * 2) : (o: number) => buffer.readInt16BE(o * 2)
+    const factor = 1 / readInt(17)
+    return {
+        xStart: readInt(0),
+        yStart: readInt(1),
+        zStart: readInt(2),
+        xExtent: readInt(3),
+        yExtent: readInt(4),
+        zExtent: readInt(5),
+        xRate: readInt(6),
+        yRate: readInt(7),
+        zRate: readInt(8),
+        xlen: readInt(9) * factor,
+        ylen: readInt(10) * factor,
+        zlen: readInt(11) * factor,
+        alpha: readInt(12) * factor,
+        beta: readInt(13) * factor,
+        gamma: readInt(14) * factor,
+        divisor: readInt(15) / 100,
+        summand: readInt(16),
+        sigma: undefined
+    }
+}
+
+function getBlocks(header: Dsn6Header) {
+    const { xExtent, yExtent, zExtent } = header
+    const xBlocks = Math.ceil(xExtent / 8)
+    const yBlocks = Math.ceil(yExtent / 8)
+    const zBlocks = Math.ceil(zExtent / 8)
+    return { xBlocks, yBlocks, zBlocks }
+}
+
+export async function readDsn6Header(file: FileHandle): Promise<{ header: Dsn6Header, littleEndian: boolean }> {
+    const { buffer } = await file.readBuffer(0, dsn6HeaderSize)
+    const brixStr = String.fromCharCode.apply(null, buffer) as string
+    const isBrix = brixStr.startsWith(':-)')
+    const littleEndian = isBrix || buffer.readInt16LE(18 * 2) === 100
+    const header = isBrix ? parseBrixHeader(brixStr) : parseDsn6Header(buffer, littleEndian)
+    return { header, littleEndian }
+}
+
+export async function parseDsn6Values(header: Dsn6Header, source: Uint8Array, target: Float32Array, littleEndian: boolean) {
+    if (!littleEndian) {
+        // even though the values are one byte they need to be swapped like they are 2
+        SimpleBuffer.flipByteOrderInPlace2(source.buffer)
+    }
+
+    const { divisor, summand, xExtent, yExtent, zExtent } = header
+    const { xBlocks, yBlocks, zBlocks } = getBlocks(header)
+
+    let offset = 0
+    // loop over blocks
+    for (let zz = 0; zz < zBlocks; ++zz) {
+        for (let yy = 0; yy < yBlocks; ++yy) {
+            for (let xx = 0; xx < xBlocks; ++xx) {
+                // loop inside block
+                for (let k = 0; k < 8; ++k) {
+                    const z = 8 * zz + k
+                    for (let j = 0; j < 8; ++j) {
+                        const y = 8 * yy + j
+                        for (let i = 0; i < 8; ++i) {
+                            const x = 8 * xx + i
+                            // check if remaining slice-part contains values
+                            if (x < xExtent && y < yExtent && z < zExtent) {
+                                const idx = ((((x * yExtent) + y) * zExtent) + z)
+                                target[idx] = (source[offset] - summand) / divisor
+                                ++offset
+                            } else {
+                                offset += 8 - i
+                                break
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+export function getDsn6Counts(header: Dsn6Header) {
+    const { xExtent, yExtent, zExtent } = header
+    const { xBlocks, yBlocks, zBlocks } = getBlocks(header)
+    const valueCount = xExtent * yExtent * zExtent
+    const count = xBlocks * 8 * yBlocks * 8 * zBlocks * 8
+    const elementByteSize = 1
+    const byteCount = count * elementByteSize
+    return { count, byteCount, valueCount }
+}
+
+async function parseInternal(file: FileHandle, size: number, ctx: RuntimeContext): Promise<Dsn6File> {
+    await ctx.update({ message: 'Parsing DSN6/BRIX file...' });
+    const { header, littleEndian } = await readDsn6Header(file)
+    const { buffer } = await file.readBuffer(dsn6HeaderSize, size - dsn6HeaderSize)
+    const { valueCount } = getDsn6Counts(header)
+
+    const values = new Float32Array(valueCount)
+    await parseDsn6Values(header, buffer, values, littleEndian)
+
+    const result: Dsn6File = { header, values };
+    return result;
+}
+
+export function parseFile(file: FileHandle, size: number) {
+    return Task.create<Result<Dsn6File>>('Parse DSN6/BRIX', async ctx => {
+        try {
+            return Result.success(await parseInternal(file, size, ctx));
+        } catch (e) {
+            return Result.error(e);
+        }
+    })
+}
+
+export function parse(buffer: Uint8Array) {
+    return parseFile(FileHandle.fromBuffer(SimpleBuffer.fromUint8Array(buffer)), buffer.length)
+}

+ 44 - 0
src/mol-io/reader/dsn6/schema.ts

@@ -0,0 +1,44 @@
+/**
+ * Copyright (c) 2019 mol* contributors, licensed under MIT, See LICENSE file for more info.
+ *
+ * @author Alexander Rose <alexander.rose@weirdbyte.de>
+ */
+
+export interface Dsn6Header {
+    /** the origin of the map in grid units, along X, Y, Z */
+    xStart: number
+    yStart: number
+    zStart: number
+    /** the extent (size) of the map, along X, Y, Z, in grid units */
+    xExtent: number
+    yExtent: number
+    zExtent: number
+    /** number of grid points along the whole unit cell, along X, Y, Z */
+    xRate: number
+    yRate: number
+    zRate: number
+    /** Unit cell parameters */
+    xlen: number
+    ylen: number
+    zlen: number
+    alpha: number
+    beta: number
+    gamma: number
+    /**
+     * Constants that bring the electron density from byte to normal scale.
+     * They are calculated like this: prod = 255.0/(rhomax-rhomin), plus = -rhomin*prod.
+     */
+    divisor: number
+    summand: number
+    /** Rms deviation of electron density map (only given in BRIX but not in DSN6) */
+    sigma: number | undefined
+}
+
+/**
+ * DSN6 http://www.uoxray.uoregon.edu/tnt/manual/node104.html
+ * BRIX http://svn.cgl.ucsf.edu/svn/chimera/trunk/libs/VolumeData/dsn6/brix-1.html
+ */
+export interface Dsn6File {
+    header: Dsn6Header
+    values: Float32Array
+}

Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio